• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Improving the efficiency of drawing

Niften

Member
Hi everybody,

I have a draw event that draws my sandbox world. It is the draw event of the chunks and it draws all blocks in its grid. How can I improve its performance with surfaces?

I've asked about optimizing the drawing and many people have pointed me toward surfaces. What can I do to improve it?

Code:
for(var xx=0; xx<CHUNK_SIZE; xx++) {
    for(var yy=0; yy<CHUNK_SIZE; yy++) {
   
        // Draw background blocks
   
        if data2[# xx,yy] > 0 {
            var sub = 0;
            draw_sprite_ext(obj_item_control.block_sprite[data2[# xx,yy]],sub,x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,1,1,0,make_color_rgb(191,191,191),1);
            depth = 1;
        }
   
        // Draw foreground blocks
   
        if (data[# xx,yy] > 0) && (is_tile(data[# xx,yy])) {
            var index = 0;
            var col = c_white;      
       
            if index != -1 {
                draw_sprite_ext(obj_item_control.block_sprite[data[# xx,yy]],index,x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,1,1,0,col,1);
            } else {
                draw_sprite_ext(obj_item_control.block_sprite[data[# xx,yy]],0,x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,1,1,0,col,1);
            }
        }
   
        if global.see_lightmap = true {
            if lmp[# xx,yy] = 0 {
                draw_rectangle_color(x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,x+xx*BLOCK_SIZE+BLOCK_SIZE,y+yy*BLOCK_SIZE+BLOCK_SIZE,c_blue,c_blue,c_blue,c_blue,false);
            } else {
                draw_rectangle_color(x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,x+xx*BLOCK_SIZE+BLOCK_SIZE,y+yy*BLOCK_SIZE+BLOCK_SIZE,c_red,c_red,c_red,c_red,false);
            }
        }

    }
}
 
Last edited:

Simon Gust

Member
Hi everybody,

I have a draw event that draws my sandbox world. It is the draw event of the chunks and it draws all blocks in its grid. How can I improve its performance with surfaces?

I've asked about optimizing the drawing and many people have pointed me toward surfaces. What can I do to improve it?

Code:
for(var xx=0; xx<CHUNK_SIZE; xx++) {
    for(var yy=0; yy<CHUNK_SIZE; yy++) {
 
        // Draw background blocks
 
        if data2[# xx,yy] > 0 {
            var sub = 0;
            draw_sprite_ext(obj_item_control.block_sprite[data2[# xx,yy]],sub,x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,1,1,0,make_color_rgb(191,191,191),1);
            depth = 1;
        }
 
        // Draw foreground blocks
 
        if (data[# xx,yy] > 0) && (is_tile(data[# xx,yy])) {
            var index = 0;
            var col = c_white;   
    
            if index != -1 {
                draw_sprite_ext(obj_item_control.block_sprite[data[# xx,yy]],index,x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,1,1,0,col,1);
            } else {
                draw_sprite_ext(obj_item_control.block_sprite[data[# xx,yy]],0,x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,1,1,0,col,1);
            }
        }
 
        if global.see_lightmap = true {
            if lmp[# xx,yy] = 0 {
                draw_rectangle_color(x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,x+xx*BLOCK_SIZE+BLOCK_SIZE,y+yy*BLOCK_SIZE+BLOCK_SIZE,c_blue,c_blue,c_blue,c_blue,false);
            } else {
                draw_rectangle_color(x+xx*BLOCK_SIZE,y+yy*BLOCK_SIZE,x+xx*BLOCK_SIZE+BLOCK_SIZE,y+yy*BLOCK_SIZE+BLOCK_SIZE,c_red,c_red,c_red,c_red,false);
            }
        }

    }
}
Before you try an make surfaces, take a minute to optimize the not draw-part of this code
Code:
var sprites = obj_item_control.block_sprite;
var bg_col = make_color_rgb(191,191,191);
var fg_col = c_white;

for(var i = 0; i < CHUNK_SIZE; i++) {
    var xx = x + i * BLOCK_SIZE;
    for(var j = 0; j < CHUNK_SIZE; j++) {
        var yy = y + j * BLOCK_SIZE;
     
        // Draw background blocks
        var tile_bg = data2[# i, j];
        if (tile_bg) {
            // get index
            var index = 0; // get_index(data2, i, j);
         
            // draw
            draw_sprite_ext(sprites[tile_bg], index, xx, yy, 1, 1, 0, bg_col, 1);
        }
 
        // Draw foreground blocks
        var tile_fg = data[# i, j];
        if (tile_fg && is_tile(tile_fg)) {
            // get index
            var index = 0; // get_index(data, i, j);
         
            // draw                 
            draw_sprite_ext(sprites[tile_fg], index, xx, yy, 1, 1, 0, fg_col, 1);
        }
 
        // lighting
        if (global.see_lightmap == true) {
            if (lmp[# i, j] == 0) {
                draw_sprite(spr_blue, 0, xx, yy);
            } else {
                draw_sprite(spr_red, 0, xx, yy);
            }
        }
    }
}
It is very important that you keep calculations outside of loops, because if they are all the same for each block, calculating them CHUNK_SIZE * CHUNK_SIZE times is
a huge waste.
Try to access only global or local variables, dot-operators are a lot slower. That means the line
Code:
var sprites = obj_item_control.block_sprite;
makes it a temporary variable, these are the fastest to access and in this case, "sprites" is a pointer towards the block_sprite array in obj_item_control.
This read-only though!

Also, never draw primitives without surfaces.
like draw_rectangle, they slow down the overall drawing by breaking the vertex batch, use coloured sprites for this.
 
Last edited:

Niften

Member
Before you try an make surfaces, take a minute to optimize the not draw-part of this code
Code:
var sprites = obj_item_control.block_sprite;
var bg_col = make_color_rgb(191,191,191);
var fg_col = c_white;

for(var i = 0; i < CHUNK_SIZE; i++) {
    var xx = x + i * BLOCK_SIZE;
    for(var j = 0; j < CHUNK_SIZE; j++) {
        var yy = y + j * BLOCK_SIZE;
     
        // Draw background blocks
        var tile_bg = data2[# i, j];
        if (tile_bg) {
            // get index
            var index = 0; // get_index(data2, i, j);
         
            // draw
            draw_sprite_ext(sprites[tile_bg], index, xx, yy, 1, 1, 0, bg_col, 1);
        }
 
        // Draw foreground blocks
        var tile_fg = data[# xx,yy];
        if (tile_fg && is_tile(tile_fg)) {
            // get index
            var index = 0; // get_index(data, i, j);
         
            // draw                 
            draw_sprite_ext(sprites[tile_fg], index, xx, yy, 1, 1, 0, fg_col, 1);
        }
 
        // lighting
        if (global.see_lightmap == true) {
            if (lmp[# xx,yy] == 0) {
                draw_sprite(spr_blue, 0, i, j);
            } else {
                draw_sprite(spr_red, 0, i, j);
            }
        }
    }
}
It is very important that you keep calculations outside of loops, because if they are all the same for each block, calculating them CHUNK_SIZE * CHUNK_SIZE times is
a huge waste.
Try to access only global or local variables, dot-operators are a lot slower. That means the line
Code:
var sprites = obj_item_control.block_sprite;
makes it a temporary variable, these are the fastest to access and in this case, "sprites" is a pointer towards the block_sprite array in obj_item_control.
This read-only though!

Also, never draw primitives without surfaces.
like draw_rectangle, they slow down the overall drawing by breaking the vertex batch, use coloured sprites for this.
Thank you for your input! I tried what you recommended and that seemed to cause the game to drop to 10 fps. I understand the concepts that you're explaining but I'm not sure what in your code triggered that.
 

Simon Gust

Member
Thank you for your input! I tried what you recommended and that seemed to cause the game to drop to 10 fps. I understand the concepts that you're explaining but I'm not sure what in your code triggered that.
Can you copy the code again, there were some i and xx / j and yy misplacements I made.
 
H

HammerOn

Guest
Surfaces will not make memory or performance more efficient. Actually, it will worsen the situation depending on the design of the level.
Surfaces are good to save the result of a dynamic effect and reuse it but a custom vertex batch would be better for your case since you are just drawing multiple instances of sprites.
I would put all sprites in the same texture page, get the uvs with sprite_get_uvs, build a custom vertex batch once when the game start and then just call vertex_submit in the draw event.
 

Simon Gust

Member
Surfaces will not make memory or performance more efficient. Actually, it will worsen the situation depending on the design of the level.
Surfaces are good to save the result of a dynamic effect and reuse it but a custom vertex batch would be better for your case since you are just drawing multiple instances of sprites.
I would put all sprites in the same texture page, get the uvs with sprite_get_uvs, build a custom vertex batch once when the game start and then just call vertex_submit in the draw event.
Surfaces are "ok" I guess if you are chunking but certainly not when drawing normally.
Vertex buffers are good, but not very dynamic, so each time a tile is changed, the whole buffer has to be reloaded, forcing smaller chunk sizes, increasing vertex breaks.
 
H

HammerOn

Guest
Surfaces are "ok" I guess if you are chunking but certainly not when drawing normally.
But if you chunk something that are multiple instances of small pieces in a grid, you are using more memory filling surfaces bigger in size with repeated blocks of pixels.
Vertex buffers are good, but not very dynamic, so each time a tile is changed, the whole buffer has to be reloaded, forcing smaller chunk sizes, increasing vertex breaks.
Yes, but this isn't an issue. Static drawing goes in a frozen vertex buffer, dynamic drawing is regular drawing and updated every step. It's just how batching works.

Btw, we could have a function to captures the internal vertex buffer created by the drawing functions. Like:
Code:
for(var i=0; i<number; ++i) {
   draw_sprite_ext(...);
}
buffer = vertex_create_buffer_from_draw(...)
It would make things easier since a custom vertex buffer for this issue is all about recreating these draw functions so you get a buffer that you can manually handle.
 

Simon Gust

Member
But if you chunk something that are multiple instances of small pieces in a grid, you are using more memory filling surfaces bigger in size with repeated blocks of pixels.
That is true, but it is faster because only drawing so much once, and then only 1 surface (per chunk).
I actually used to do it like this and it was fine I guess.

Yes, but this isn't an issue. Static drawing goes in a frozen vertex buffer, dynamic drawing is regular drawing and updated every step. It's just how batching works.
I am aware. Frozen vbs are good but submitting a vb, frozen or not will still break the batch per chunk. It isn't really an issue in GMS 2, but in GMS 1.4, batch breaks are a slowdown.
Apart from that, a buffer would really come in handy to reload the vb each time.
I'd say this is one of the best methods for tile rendering. Gives me a headache though.
 

Niften

Member
Thanks for the input everybody. I looked into vertex buffers in the manual - how would I begin implementing this? I'm assuming I would create a vertex buffer, create primitives for each block and apply a texture.
 
Last edited:

Simon Gust

Member
Thanks for the input everybody. I looked into vertex buffers in the manual - how would I begin implementing this?
First of all you want to read the 4th part of shader overview
https://www.yoyogames.com/blog/18/shaders-overview-part-4

So, each chunk will have 1 vertex buffer, inside it there are all the graphics (sprites). But you have to first define a vertex format.
Without it, the buffer has no idea how to draw the stuff you gave it. A vertex format is a one time thing and can be customized like whatever you want it to be.
Code:
/// CREATE EVENT OF A CONTROLING OBJECT
vertex_format_begin();
vertex_format_add_position();
vertex_format_add_textcoord();
vertex_format_add_colour();
global.vertex_format = vertex_format_end();
This format also tells you in what order you have to put in the data for the vertex buffer, any missordering and it will not work properly.
For your code you want the position (where to draw), the textcoord (what sprite to use) and colour (for lighting and backgrounds).
This format is saved to the global variable global.vertex_format.

Next, each chunk should have it's own vertex buffer.
Code:
/// CREATE EVENT OF THE CHUNK OBJECT
vertex_buffer = noone;
As you can see, I put it to noone because you can only actually do this vertex buffer thing in the draw event.

Then to the actual drawing, which is a one time thing.
Code:
if (vertex_buffer == noone)
{
    // create vertex buffer
    vertex_buffer = vertex_create_buffer();
   
    // get premature data
    var sprites = obj_item_control.block_sprite;
    var bg_col = make_color_rgb(191,191,191);
    var fg_col = c_white;
    var b = vertex_buffer;
   
    // start drawing
    vertex_begin(vertex_buffer, global.vertex_format)
   
    for(var i = 0; i < CHUNK_SIZE; i++)
    {
        var xx = x + i * BLOCK_SIZE;
        for(var j = 0; j < CHUNK_SIZE; j++)
        {
            var yy = y + j * BLOCK_SIZE;
         
            // Draw background blocks
            var tile_bg = data2[# i, j];
            if (tile_bg) 
            {
                // get index
                var index = 0; // get_index(data2, i, j);
             
                // get sprite coordinates on texture page
                var uvs = sprite_get_uvs(sprites[tile_bg], index);
               
                // draw triangles
                // 1st triangle....
                vertex_position(b, xx, yy);
                vertex_texcoord(b, uvs[0], uvs[1]);
                vertex_colour(b, bg_col, 1);
               
                vertex_position(b, xx + BLOCK_SIZE, yy);
                vertex_texcoord(b, uvs[2], uvs[1]);
                vertex_colour(b, bg_col, 1);
               
                vertex_position(b, xx + BLOCK_SIZE, yy + BLOCK_SIZE);
                vertex_texcoord(b, uvs[2], uvs[3]);
                vertex_colour(b, bg_col, 1);
               
                // 2nd triangle....
                vertex_position(b, xx + BLOCK_SIZE, yy + BLOCK_SIZE);
                vertex_texcoord(b, uvs[2], uvs[3]);
                vertex_colour(b, bg_col, 1);
               
                vertex_position(b, xx, yy + BLOCK_SIZE);
                vertex_texcoord(b, uvs[0], uvs[3]);
                vertex_colour(b, bg_col, 1);
               
                vertex_position(b, xx, yy);
                vertex_texcoord(b, uvs[0], uvs[1]);
                vertex_colour(b, bg_col, 1);
            }
     
            // Draw foreground blocks
            var tile_fg = data[# i, j];
            if (tile_fg && is_tile(tile_fg)) 
            {
                // get index
                var index = 0; // get_index(data, i, j);
             
                // get sprite coordinates on texture page
                var uvs = sprite_get_uvs(sprites[tile_fg], index);
               
                // draw triangles
                // 1st triangle....
                vertex_position(b, xx, yy);
                vertex_texcoord(b, uvs[0], uvs[1]);
                vertex_colour(b, fg_col, 1);
               
                vertex_position(b, xx + BLOCK_SIZE, yy);
                vertex_texcoord(b, uvs[2], uvs[1]);
                vertex_colour(b, fg_col, 1);
               
                vertex_position(b, xx + BLOCK_SIZE, yy + BLOCK_SIZE);
                vertex_texcoord(b, uvs[2], uvs[3]);
                vertex_colour(b, fg_col, 1);
               
                // 2nd triangle....
                vertex_position(b, xx + BLOCK_SIZE, yy + BLOCK_SIZE);
                vertex_texcoord(b, uvs[2], uvs[3]);
                vertex_colour(b, fg_col, 1);
               
                vertex_position(b, xx, yy + BLOCK_SIZE);
                vertex_texcoord(b, uvs[0], uvs[3]);
                vertex_colour(b, fg_col, 1);
               
                vertex_position(b, xx, yy);
                vertex_texcoord(b, uvs[0], uvs[1]);
                vertex_colour(b, fg_col, 1);
            }
        }
    }
   
    // end drawing
    vertex_end(vertex_buffer)
    vertex_freeze(vertex_buffer);
}
else
{
    var texture = sprite_get_texture(obj_item_control.block_sprite[0]);
    vertex_submit(vertex_buffer, pr_trianglelist, texture);
}
So drawing consists of drawing 2 triangles for a sprite, each triangle has 3 points that require data, this data is like the vertex format suggests:
position, textcoord and colour.
Then before the loops start, vertex_begin() has to be called.
After the loops vertex_end() has to be called and if you are feeling warm you can also freeze it (makes it read-only and faster).
Then the vertex buffer is filled, and the code will branch into the else statement.
There a texture has to be given, I gave it the texture page for the first sprite inside your block_sprite array.
Finally you submit the buffer.

So, are you sure this is how you want to do it?
 
there's no reason that I know of that you can't build a vertex buffer outside of the draw event. It isn't drawing anything.

If your chunks are not static, then a normal static vertex buffer isn't going to be too much of an advantage. Every time a change happens, you will need to rebuild the whole buffer. How can you get around that? You could use a shader to read a texture that contains a map of your chunks, each pixel a different "block", and then draw the blocks based on what value is read out of each pixel. In fact, you wouldn't necessarily need to draw more than two triangles, and then you wouldn't even need a vertex buffer for that, you could just draw any rectangle as long as you have a way of determining the position of each fragment within the rectangle. That starts to get a little technical, but I've done things like that before and its let me get away with drawing millions of tiles at once with basically no performance impact.

if the contents of your chunks are static, then you wouldn't need to worry about any of that though.
 

Simon Gust

Member
there's no reason that I know of that you can't build a vertex buffer outside of the draw event. It isn't drawing anything.

If your chunks are not static, then a normal static vertex buffer isn't going to be too much of an advantage. Every time a change happens, you will need to rebuild the whole buffer. How can you get around that? You could use a shader to read a texture that contains a map of your chunks, each pixel a different "block", and then draw the blocks based on what value is read out of each pixel. In fact, you wouldn't necessarily need to draw more than two triangles, and then you wouldn't even need a vertex buffer for that, you could just draw any rectangle as long as you have a way of determining the position of each fragment within the rectangle. That starts to get a little technical, but I've done things like that before and its let me get away with drawing millions of tiles at once with basically no performance impact.

if the contents of your chunks are static, then you wouldn't need to worry about any of that though.
We've already talked about that, using vertex_buffer_create_from_buffer(), vertex buffers get kind of dynamic but it also gives headaches.
 

Simon Gust

Member
yes, but which part did you already talk about? I didn't see anybody suggest encoding the map into a texture, unless I'm blind.
The thing with the other thing about reloading vertex buffers being a niusance. But buffers would pretty much handle this, as I said -> headaches.
So why don't we talk about your idea some more and have a nice chat.
 
I'll write up an exmaple, and then share it. I'll do the simpler version where each tile is its own quad, becuase less can go wrong with that approach.

EDIT: actually decided to go the method where no vertex buffer is needed.

Oh, about switching between buffers and vertex buffers, I've found to convert one to the other is REALLY slow. Almost as bad as just rebuilding the vertex buffer.
 
Last edited:

Simon Gust

Member
I'll write up an exmaple, and then share it. I'll do the simpler version where each tile is its own quad, becuase less can go wrong with that approach.

Oh, about switching between buffers and vertex buffers, I've found to convert one to the other is REALLY slow. Almost as bad as just rebuilding the vertex buffer.
nice
In the meantime I'd like to also take another method into consideration.
I don't know what it is called but it kinda sounds similar like your method.
Basically, there are no chunks anymore, just a surface that is a bit bigger than the screen but divided by the tile size.
Those are like 128x128 surfaces, so no real memory problems.
On this surface, a tile in the real world is a pixel on the surface. Using the red green and blue component, we can differentiate what sprite and what index is drawn later.
This surface (texture) is submitted to a fragment shader that just does everything for you and out comes the result.

The performance on this depends on if you only update strips as you're moving or the whole surface and how big that surface is.
Generally, drawing draw_point_colour() is 4 times faster than draw_sprite. The shader will barely affect performance too.
It requires a manually created tile page and a canvas for drawing that isn't the application surface.
 
I think we are on the same page here. I've got a test you can run. No need to build a vertex buffer with this. I just draw the tile map stretched out so that each pixel in the tile map is the size of 1 tile. Also no need to turn off texture interpolation, becuase the shader is designed to work with texture interpolation.

Here's a test tile set. The tiles are 32x32, but they have a 1 pixel border padding around their edges so that the edges don't bleed, thus only 3x3 tiles can fit on this 128x128 texture. IMPORTANT: this background needs to be marked "used for 3d"!
https://imgur.com/VYznTfj

now to set up a test map run this once:
Code:
    chunk_surf = -1;
    map_size = 512;
    map_area = map_size * map_size;
    var _buffer_size = map_size * map_size * 4;
    chunk_buffer = buffer_create(_buffer_size,buffer_fast,1);
    buffer_seek( chunk_buffer, buffer_seek_start, 0);
    for (var i = 0; i < map_area; i += 1) {
        buffer_write( chunk_buffer, buffer_u8, i mod 3 );
        buffer_write( chunk_buffer, buffer_u8, (i div 3) mod 3 );
        buffer_write( chunk_buffer, buffer_u8, 0 );
        buffer_write( chunk_buffer, buffer_u8, 255 );
    }
    tex_tiles = background_get_texture(bck_tiles);
    tile_size = 32;
    sh_map_tex_tiles = shader_get_sampler_index( sh_map, "tex_tiles" );
    sh_map_map_size = shader_get_uniform( sh_map, "map_size");
    sh_map_tile_size = shader_get_uniform( sh_map, "tile_size");

In draw event, if surface doesn't exist then create surface and copy buffer to surface. Then draw the surface:
the map_size uniform is pretty self explanatory.
but the tile_size uniform probably needs elaboration. The x component is the distance between tiles dividied by tile set size multiplied by 255. The y component is 1 dividied by tile set size. The z component is the tile size divided by the tile set size. You might ask what is the difference between x and z? The difference is the x component is measuring the distance across a tile including its border padding, but the z component does not include the border. Note: added a multiplication by map_size to tile_size.z uniform (to avoid having to do it for every fragment in the shader).
Code:
    if (!surface_exists(chunk_surf)) {
        chunk_surf = surface_create(map_size,map_size);
        buffer_set_surface( chunk_buffer, chunk_surf, 0, 0, 0 );
    }
    shader_set(sh_map);
    texture_set_stage( sh_map_tex_tiles, tex_tiles );
    shader_set_uniform_f( sh_map_map_size, map_size, 1/map_size, 0.5/map_size);
    shader_set_uniform_f(sh_map_tile_size, 34/128 * 255, 1/128, map_size * 32/128 );
    draw_surface_ext(chunk_surf, x, y, 32, 32, 0, c_white, 1);
    shader_reset();
note: can draw surface anywhere. I am drawing it at (x,y) because I am scrolling the map by adding/subtracting to (x,y).

the vertex shader can be just pass-thru, but the fragment shader is this:
Code:
varying vec2 v_vTexcoord;
uniform sampler2D tex_tiles;
uniform vec3 map_size;
uniform vec3 tile_size;
void main() {
    vec2 map_coord = floor(v_vTexcoord * map_size.x) * map_size.y;
    vec2 tile = texture2D(gm_BaseTexture, map_coord + map_size.z).bg;
    vec2 tile_coord = tile * tile_size.x + tile_size.y + (v_vTexcoord - map_coord) * tile_size.z;
    gl_FragColor = texture2D( tex_tiles, tile_coord );
}

And here's the result in game:
https://i.imgur.com/G5p1NMB.png

edit: made a small optimization by moving one of the multiplications out of the shader.
 
Last edited:

Niften

Member
I'll do anything it takes to get the performance up. Will I be able to use sprites that are transparent in places? Maybe that doesn't matter. In regards to the other method you mentioned: maybe chunks could be changed to 128x128 surfaces with pixels as the blocks? I'm worried about using RGB for it though because there are well over 255 block indexes and a chunk needs to store three values in each cell: foreground block, background block, and the metadata. Probably more efficient way for doing that as well. There's a ton of stuff that needs to be optimized.

You also say that it is drawn once. What if a block is broken? What if a block is placed?

In regards to whether I should do it, what do you think would be the best method considering the system I have now? Right now, I have three ds_grids, one that stores the fg, one for the bg, and one for the metadata. Metadata is stored using make_color_rgb so that it can store three "tags" (durability, etc.).

In regards to the surface method you've suggested, the world is supposed to be infinite so I'm not sure how that would work. My world generation scripts also work with ds_grids. I'm just trying to perfect what I'm doing because there's no way this game should be running at what it is now.

Thank you everyone for all of the input.
 
Last edited:

Niften

Member
there's no reason that I know of that you can't build a vertex buffer outside of the draw event. It isn't drawing anything.

If your chunks are not static, then a normal static vertex buffer isn't going to be too much of an advantage. Every time a change happens, you will need to rebuild the whole buffer. How can you get around that? You could use a shader to read a texture that contains a map of your chunks, each pixel a different "block", and then draw the blocks based on what value is read out of each pixel. In fact, you wouldn't necessarily need to draw more than two triangles, and then you wouldn't even need a vertex buffer for that, you could just draw any rectangle as long as you have a way of determining the position of each fragment within the rectangle. That starts to get a little technical, but I've done things like that before and its let me get away with drawing millions of tiles at once with basically no performance impact.

if the contents of your chunks are static, then you wouldn't need to worry about any of that though.
And yes, chunks are not static. A question about this method: How would we translate the color into a particular block ID? There are well over 255 block IDs, so maybe adding the RGB components together? This would mean I wouldn't be able to have more than 765 IDs. Not sure what the best way would be...
 
And yes, chunks are not static. A question about this method: How would we translate the color into a particular block ID? There are well over 255 block IDs, so maybe adding the RGB components together? This would mean I wouldn't be able to have more than 765 IDs. Not sure what the best way would be...
you could have 256 * 256 = 65536 block ids, but more practically, you are limited by the size of your tile set texture, and the size of a tile. For example, if you have a 1024 x 1024 tile set, and each tile is 34 pixels across (including border), then you have a total of 900 tiles on a tile set.

tile id's are encoded into the surface in the following way:

the column of the tile is held in the blue channel.
the row of the tile is held in the green channel.

so blue 0 and green 1, would be first column, second row.

to change a tile, you would need to do two things. First draw a pixel onto the tile map at the tile position, the color of this pixel based on the column and row of the tile (as outlined above). Second, rewrite the blue and green channel positions for the tile in the buffer (same colors). That way if your surface ever goes volatile, you can rebuild it by simply recopying the buffer to the surface.

By the way, chunks can be LARGE. At least 512x512, I'm almost certain 1024x1024 will work on windows machines. Past that size, there might be problems with floating point precision in the fragment shader.

edit: pretty sure having transparent parts of tiles will be okay. The one downside of this method is you will have to manually compile all of your tiles onto one texture (background or sprite with one sub image). note: the tile set background/sprite must be marked "used for 3d".

EDIT:

if you'd like to see a project that has a full example of this method, check out this link. file is about 15kb, and is for GMS1.4x. Press arrow keys to scroll map. Left click anywhere to change a tile.
https://www.dropbox.com/s/kqy51l26uli7y92/simple_tile_map_shader.gmz?dl=0
 
Last edited:

Niften

Member
So I absolutely have to use tilesets? I would much rather have the color represent a number which can be put into the sprite array to find out what sprite to use.
 
If you mean with my method, then yeah, I can't see a way that you could just use a bunch of normal sprites. Maybe it isn't exactly the easiest method to set up, but it is super fast. Are you blocks animated or something? How come you don't want to compile them into one tileset texture?

You could do what @Simon Gust suggests, which will allow you to use sprite uvs. Maybe you will consider that more flexible, but it will surely be at least a little slower, especially when modifying the map.

You know, you could just use gamemaker's built it tile system if you'd like. It's relatively fast, and I'm sure you'd find it less complicated. Plus, you can change the tiles at any time... although I don't know what performance impact is caused by changing tiles... I'm guessing not that much.
 

Niften

Member
Maybe instead of using the R G B values to calculate the rows and such, maybe they could be added to get a tile ID?

var block_id = color_get_red(col)+color_get_blue(col)+color_get_green(col);
var sprite = block_sprite(block_id)

Also, how many of these "surface chunks" could exist at one time without hurting performance?
 
I don't understand your question about color and sprite.

About your second question, if your question is how many surface chunks can be drawn at once, then I'd say probalby quite a few, most of them will not be in the view, in which case the fragment shader will not even run for them. Still, it would be a good idea to only draw chunks that actually intersect with the view. The size of a chunk will not have much effect on how quickly it draws. The number of chunks that can exist in memory at once is only limited by the amount of memory avialable to gamemaker, and that would depend on the size of each chunk. In the project I linked above, the chunk is 512x512 blocks (I imagine larger than you'd end up using in your game), and each of those 512x512 chunks will use about 2MB for the surface and buffer combined. Smaller chunks will use less, of course. So, even with those massive chunks, you could have quite a few of them loaded in memory at the same time before you run out of memory.

All of that only pertains to the drawing of the chunks. I expect you will want to store extra data about your chunks in some other way. Although, if you can fit all the data for each block into the remaining 2 (unsigned) bytes, there's no reason you couldn't use the buffer as the principal structure for the storage of your chunks. You can read and write buffers directly to disk, so that would make saving and loading rather straightforward.
 

Simon Gust

Member
I'm gonna explain my method a bit more.
So, it also uses colors.
red can be x on the tilepage
grn can be y on the tilepage
blu can be random frame (like rotated block, or mirrored)

This means you have 65536 images, which don't exactly count as sprites but depending on how many image_indexes you have it can be still a few.
If each sprite has 16 image_indexes, you still have 4096 sprites. These can easily just go on one big texture page. (4096 x 4096).

But first you have to create a tilepage, because your texture page is always shuffled.
I recommend you make your own as a sprite and not generated as a surface because memory.

Editing is also easy, you just have to draw 1 pixel, thats all. Deleting a pixel isn't hard either, you can just draw a black pixel.
It is really hard to make an example that works with your code and setup so I will just link you to this here
https://forum.yoyogames.com/index.php?threads/vertex-drawing-clunkiness.25367/
It would be cool if that is is enough for you to understand the concept so you can code your own renderer.
 

Niften

Member
Yeah I totally understand the concept. So I assume I'd just use a loop to draw it, but I'm just confused about the other stuff. Wouldn't it be pretty slow as you're reading the color values of every pixel in a 128x128 shader and drawing it? Am I missing something here?
 

Simon Gust

Member
Yeah I totally understand the concept. So I assume I'd just use a loop to draw it, but I'm just confused about the other stuff. Wouldn't it be pretty slow as you're reading the color values of every pixel in a 128x128 shader and drawing it? Am I missing something here?
Nah, shaders are superfast, The shader barely has to do more than it is already doing if you draw without setting a shader. The app surface goes through a default shader that still draws all pixels.
And here, the same thing happens. Each pixel has it's own "cpu" to process, making parallel computation super fast on a shader.
 

Simon Gust

Member
So, in my implementation of this, if I let the rendering happen every step (generally don't do that)
I get 1000 fps. This includes rendering foreground tiles, background tiles, liquid tiles and lighting at the same time.
The amount of vertex breaks is mostly because of the rendering every step. Most of them are for the lighting where I copy a surface 24 times at once.
And no, my pc isn't a beast.
Tested with YYC - GMS 1.4
upload_2017-12-10_10-3-27.png
 

Niften

Member
Do you have a separate surface for fg/bg/liquids? What would you recommend is the best way to incorporate metadata for each tile?
 

Simon Gust

Member
Do you have a separate surface for fg/bg/liquids? What would you recommend is the best way to incorporate metadata for each tile?
I have some 128x128 surfaces.
1 x tile rendering
1 x wall rendering
1 x liquid rendering
1 x light rendering
1 x temp (for copy loop)

My metadata is bitmasking.
tile type: 10 bytes
wall type: 10 bytes
Liquid type: 8 bytes
Liquid amount: 8 bytes
and some other stuff that doesn't do very much.
 

Niften

Member
Okay, I think I know how I want to do this. Like Simon's method, I'll be using several surfaces for each chunk, one for tiles, background blocks, metadata, etc. Instead of using the RGB values to calculate rows in a tileset, I want to use them to get a specific number. Each block in the game has an index attributed to it and I want to be able to store that number in one of the pixels. I would then return the index and have the game draw the sprite that the index has set. How would I approach this?
 

Simon Gust

Member
Yes, so the renderer is not chunked and therefore requires a player position to render around him.
This position must be clamped and divided by BLOCK_SIZE
The area this renderer takes up and what size the surfaces will be depends on resolution and BLOCK_SIZE.
if BLOCK_SIZE is 16, the area should be 128x128
if BLOCK_SIZE is 32, the area can be 64x64.

The position is to be centered on the player - half the area
Code:
var gridX = clamp((obj_player.x div 16 - 64), 64, wdt - 64);
var gridY = clamp((obj_player.y div 16 - 64), 64, hgt - 64);
Clamping, so the array with the metadata is not read outside the world, crashing the game.

With this position you can fill a surface with pixels representing to the metadata.
Code:
surface_set_target(surface);
draw_clear(c_black);

// fill surface
for (var i = 0; i < 128; i++)
{
    var xx = gridX + i;
    for (var j = 0; j < 128; j++)
    {
        var yy = gridY + j;
       
        // get metadata
        var tile = metadata[@ xx, yy];
        var foreground = getType(tile);
        if (foreground != 0)
        {
            var frame = frame4WayType(xx, yy, foreground);
            var col = make_colour_rgb(foreground, 0, 0);
            draw_point_colour(i, j, col);
        }
    }
}

surface_reset_target();
foregroundTexture = surface_get_texture(surface);
The last line is required as shaders need textures instead of surface indexes.

the script getType() should be just an easy way to get the right bits from the metadata
the script frame4WayType() is one I made to frame for me
Code:
var i = argument0;
var j = argument1;
var mid = argument2;

if (!mid) return (0);

var red = (4 * mid) mod 32 + 1;
var grn = (4 * mid) div 32 + 1;
var blu = random(255);

// check lft
var lft = metadata[@ i-1, j];
if (getType(lft)) red += 2;

// check rgt
var rgt = metadata[@ i+1, j];
if (getType(rgt)) red -= 1;

// check top
var top = metadata[@ i, j-1];
if (getType(top)) grn += 2;

// check bot
var bot = metadata[@ i, j+1];
if (getType(bot)) grn -= 1;

return (red | (grn << 8) | (blu << 16));
With this filled texture you can already start rendering.
draw event code
Code:
var draw_gridX = gridX * 16 - (resolutionX / 2);
var draw_gridY = gridX * 16 - (resolutionY / 2);

shader_set(tileRenderShader);
texture_set_stage(TRSamplerMain, foregroundTexture);
draw_sprite(spr_tile_renderer, 0, draw_gridX, draw_gridY);
shader_reset();
Resolution probably 1920 x 1080.
the spr_tile_renderer should be 2048 x 2048 in size.

Shader code
Code:
/// tileRenderShader
//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D mainSampler; // metadata texture
uniform sampler2D tileSamplerTexture; // texture with sprites for tiles on it

float mX = 1.0 / 2048.0; // size of sprite texture
float mY = 1.0 / 2048.0;

float fX = 1.0 / 128.0; // size of metadata texture
float fY = 1.0 / 128.0;

void main()
{ 
    // main data
    vec4 baseCol = texture2D( mainSampler , v_vTexcoord );
    float red = (baseCol.r * 255.0); // x on sprite texture
    float grn = (baseCol.g * 255.0); // y on sprite texture
    float blu = (baseCol.b * 255.0); // random frame
   
    // inPixel data
    vec2 inPixel = vec2(mod(v_vTexcoord.x, fX) , mod(v_vTexcoord.y, fY)); 
    // so that each pixel of a 32x32 block 
    // corresponds to the right pixel on the sprite texture
   
    // final frame
    vec2 frame = vec2((red * 16.0), (grn * 16.0));
    vec2 index = vec2(mod(frame.x, 4.0), mod(frame.y, 4.0));
   
    // pixel position on texture page
    float xx = frame.x * mX + inPixel.x;
    float yy = frame.y * mY + inPixel.y;
    vec2 position = vec2(xx, yy);
    vec4 color = texture2D( tileSamplerTexture, position );
    gl_FragColor = color * light;
}
This shader code does not include random frames nor lighting.
For this to work correctly, your sprite texture should look like this
upload_2017-12-16_15-56-22.png
This is filled for me. But the actual size should be 1024 x 1024.
Also, mine is for 16x16 tiles.
 

Niften

Member
The world isn't chunked? How would it save, then? If I want an infinite world? That part is a bit confusing to me.
 

Niften

Member
I don't understand your question about color and sprite.

About your second question, if your question is how many surface chunks can be drawn at once, then I'd say probalby quite a few, most of them will not be in the view, in which case the fragment shader will not even run for them. Still, it would be a good idea to only draw chunks that actually intersect with the view. The size of a chunk will not have much effect on how quickly it draws. The number of chunks that can exist in memory at once is only limited by the amount of memory avialable to gamemaker, and that would depend on the size of each chunk. In the project I linked above, the chunk is 512x512 blocks (I imagine larger than you'd end up using in your game), and each of those 512x512 chunks will use about 2MB for the surface and buffer combined. Smaller chunks will use less, of course. So, even with those massive chunks, you could have quite a few of them loaded in memory at the same time before you run out of memory.

All of that only pertains to the drawing of the chunks. I expect you will want to store extra data about your chunks in some other way. Although, if you can fit all the data for each block into the remaining 2 (unsigned) bytes, there's no reason you couldn't use the buffer as the principal structure for the storage of your chunks. You can read and write buffers directly to disk, so that would make saving and loading rather straightforward.
Quick question about your code.
Code:
    _i += buffer_read( map_buffer, buffer_u8) * 3;
//--------------------------------------------------------------------------   
    //advance by 1
    _i += 1;
    var _c = _i mod 3;
    var _r = (_i div 3) mod 3;
//--------------------------------------------------------------------------   
    //write new value to buffer
    buffer_seek( map_buffer, buffer_seek_relative, -2 );
    buffer_write( map_buffer, buffer_u8, _c );
    buffer_write( map_buffer, buffer_u8, _r );
This isn't practical at all. If I want to change a block I have to find the row and column that the block is in? Doesn't seem practical.
 

Niften

Member
Why does that seem impractical?
It's just not practical. Doesn't work well with a sandbox - it would work way better with item indexes. I could make an array holding the row/column of each block which would probably be better, but this would restrict me to a strict size for each tile. What if I want a bed that takes up two normal blocks? There's a good few problems with this and it would work way better with separate sprites.
 
It's just not practical. Doesn't work well with a sandbox - it would work way better with item indexes. I could make an array holding the row/column of each block which would probably be better, but this would restrict me to a strict size for each tile. What if I want a bed that takes up two normal blocks? There's a good few problems with this and it would work way better with separate sprites.
yes, you would be restricted to using uniform sized tiles. However there is the possibilty that you could use multiple tiles to represent an object of a different shape. You'd have to find some way of working around whatever limitations there are, but of course, the advantage would be the map would draw super fast. So, there are pros and cons.

Maybe you would find it easier to use the built-in tile system.
 
Top