• 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!

Legacy GM Storing surfaces... in RAM?

DukeSoft

Member
Hey'all!

I have this top-down game, in which I render the floor and the walls with specific textures, to make it look snappy. All works well - room is split up in square surfaces (say, 2048x2048) and on that I render the map once.

Then when its rendered, I draw those surfaces on the floor of the game, if needed. Works great!

Allthough - once I start getting big maps (say, 20.000 x 20.000) I'll have 10X10 of those surfaces. Not an issue persé because I give the player the opportunity to scale the surfaces, so you'll have 10x10 1024x1024 surfaces - BUT, it seems like these surfaces get stored in the GPU's ram (allthough the general memory usage of the game goes up as well? Don't know how all that is calculated, but okay).

Once GM goes over the limit of my videocard's memory (2GB), surface_create() hangs for a long time, then continues, and the surface ends up not existing.

I know its crazy to have 100 2000x2000 surfaces in memory at all times, so I was wondering, can I store them somewhere else?

Rendering takes up quite a bit of time - and rendering once the player is near is not an option, as this will result in framedrops.

Storing the surface on the disk is also not a good idea, because of disk IO times being very slow.

So, my idea was - store all non-drawn surfaces in RAM, and get them into the GPU once i need to draw them (ie, the view is near).

Is this.. possible? Did anyone here have similar problems which they tackled? I have no idea how to work on this and make the game support 20.000x20.000 maps with high-quality surfaces.
 

DukeSoft

Member
Hold up... I might be up to something -> background_create_from_surface()

I'm going to see if this works -> render 1 2048x2048 at a time (as i do now), but save it in a background instead of keeping it in the surface.
 

DukeSoft

Member
Okay, test results came back;

Yes, it works. It takes up much less GRAM.

No, its not practical. Seems like its doing the texture swaps and the rendering is 100x slower (yes, really), and initially the FPS of the game is _VERY_ low.

Guess I'll stick to the old surface version!
 

Hyomoto

Member
I've been having issues dealing with a similar problem but the way I handle it is only drawing the visible parts of the map to a surface to begin with. My issue is this ends up being kind of slow because I can end up drawing a thousand tiles to the screen and from profiling it seems the loop is the longest part of this action and I can't find a great way to reduce that. I run a separate culling operation to help with this but it means I end up with two massive loops. I combat that by setting my room speed to twice my target FPS and performing the culling in one frame and the drawing in another but it isn't a perfect solution. I've considered using your exact approach to combat this problem and draw once and store in memory, but my problem is it seems like a poor solution because I have no guarantee a surface exists in memory and if it doesn't it has to be redrawn anyways and the game could hang unexpectedly. I honestly wish I had more feedback to give, but my solution to your issues is to cull out the non-visible parts of the map and simply redraw it each frame. This isn't a perfect solution for the reasons I've mentioned but it does get around the VRAM issues, which is important because other players may have less VRAM than you do. In my case, I can combat the speed by using 16x16 tiles. It reduces the size of my loops and as long as I'm drawing under a thousand tiles the approach is generally speedy enough for me.
 

DukeSoft

Member
I think i have the solution for myself, for now. Doing this all in-game is crap... It just won't work well unless you have a very new videocard.

One solution that seems to work (and what i think a lot of big developers do as well) is pre-baking it.

Some facts:
- Rendering the view only costs A LOT of GPU power, and drops my FPS to about 15.
- Using a set of surfaces throughout the entire room (like 3x3 2048x2048 surfaces) works, but takes up a lot of GRAM - so it works, but only if you have a lot of VRAM. 12288x12288 rooms will use ~700MB of vram.
- Using buffers is crap - GM doesn't support loading surfaces from buffers, and can only save surfaces to buffers on windows and PS4.
- Rendering to a 2048x2048 surface, saving that one as a background using background_create_from_surface is VERY slow (takes about 2 seconds for 1 surface)
- Using 3x3 2048x2048 backgrounds AS TILES is _really_ fast.
- Loading a 2048x2048 PNG from file into a background takes about 200ms on a regular 7200rpm disk

Conclusion:
When saving the map, it renders, saves it to file (PNG.... because GM doesn't support JPG). This takes about 2 seconds per 2048x2048 file (probably the same source as background_create_from_surface)
When playing a game, you load up every file, add it as a background, and put it on a tile layer.

End result:
I'm going to support 3 versions.
Version 1, For people with low to avarage graphics cards:
-The maps have to be prebaked. Will take 20 seconds the first time, because all files have to be saved. Rendering also happens there (allthough rendering is like 1% of the time). After that, loading a map will use the tile / file setup and loading takes about 5 seconds. Takes up quite a bit of ram though, but, we do have 100% quality resolution terrain backgrounds!

Version 2, for people with better graphics cards (2gb + vram);
- When the map opens, it renders it to 2048x2048 surfaces (or bigger) and stores it in surfaces. The rendering will take a fraction of the time (200ms on my machine - and its not even high-end).
Good thing is, we can now also use the surfaces to draw particles to (so they will remain forever without eating CPU cycles).

Version 3, people with overall low PC's
Same as 2, but downscaled. The surfaces will be 512x512, rendering happens at the same time, but it will end up using less VRAM.
 

NightFrost

Member
I use the map into background route (through a surface) in a project of mine, but I also shamelessly throw a progress bar at the player's face. Generating the dungeon already takes some time (a modified random walk with progressively increasing amount of walkers) so a little more to create a background is not noticeable. The map size caps aroud 3200x3200 though.
 

zbox

Member
GMC Elder
You can just convert a surface to a background? Does exactly what you want - stores surface in ram. And you can transfer that to storage when you need :)
 

GMWolf

aka fel666
You could check out @Fel666 's surface manager.
https://marketplace.yoyogames.com/assets/4536/surface-manager

Not sure if it'll do what you need as I haven't really looked into it, but it claims to have the ability to reclaim lost surfaces so check it out.
It doesnt. (Nor do i claim it does).
It simply ensures that a surface is kept in memory. If it is lost, it will recreate it, but the data stored on it willl be lost.

As @zbox said, converting to a background would work.
Using buffers is also a good option,.

The real problem is that you are trying to render many large textures.
Rendering all walls and ground to textures so that it would run faster is the worst idea!
Just draw your ground and walls normally. You are over thinking this.
Alternativly, check out my optimized decals asset on the marketplace.
 
Last edited:

DukeSoft

Member
You can just convert a surface to a background? Does exactly what you want - stores surface in ram. And you can transfer that to storage when you need :)
I know - but that takes a LOT of time. Like i said, 2 seconds.


It doesnt. (Nor do i claim it does).
It simply ensures that a surface is kept in memory. If it is lost, it will recreate it, but the data stored on it willl be lost.

As @zbox said, converting to a background would work.
Using buffers is also a good option,.

The real problem is that you are trying to render many large textures.
Rendering all walls and ground to textures so that it would run faster is the worst idea!
Just draw your ground and walls normally. You are over thinking this.
Alternativly, check out my optimized decals asset on the marketplace.
I'm not sure what you mean with "rendering large textures". The terrain textures are 8 kinds of 1024x1024 maps -nothing really changes if i make them 128x128.

And what you mean with "draw your ground and walls normally" - yeah, thats not an option. The walls are 100% black and shaped like a wall - the concrete texture is overlayed so it looks like a non-repeating wall.

Same for the ground. My terrain objects only consist out of big white spheres with 255 alpha on the outside and 0 alpha on the inside.

I draw to multiple surfaces to get the "texture splatting" effect - and doing this is realtively slow. 16 fps on my PC while 500 fps when using the tilebased solution.
 

GMWolf

aka fel666
Use shaders to get your overlay system...
If you need help putting one of those toghether, ill be quite happy to give you an example. Its quite simple.

I already have one to blend two textures toghether using a blend map.

for your sustem, i would say the best solution is as follows:
Every step, draw your white circle to a surface in screen space.
Then draw that surface to screen using a shader to blend between two or more textures depending on the colour of the base texture.
 

DukeSoft

Member
Some benchmarks;

I have about 100 terrain objects - All kinds of shapes - plain white square, white circles, white alpha-gradient circles. Each object has a pointer - to which texture it should use. (e.g. Sand, Grass, Wood, Stone etc.)

This is the background_create_from_surface solution;
obj_render_object:
Code:
///CREATE
s = 2048; //Surface size
rendersingle = surface_create(s,s);
rendersingle2 = surface_create(s,s);
rendersingle3 = surface_create(s,s);
drawn = false;

///DRAW EVENT
if (drawn == false) {
    for (xx = 0; xx < ceil(room_width/s); xx++) {
        for (yy = 0; yy < ceil(room_height/s); yy++) {
            var xoffset = xx*s;
            var yoffset = yy*s;
            surface_set_target(rendersingle3);
            // In all this code global.surface_scale = 1 as I want to get rid of the scaling actually..
            draw_background_tiled_ext(global.terrain[global.default_terrain, TERRAIN_TEXTURE], (-xoffset)*(1/global.surface_scale), (-yoffset)*(1/global.surface_scale), 1/global.surface_scale, 1/global.surface_scale, c_white, 1);
            surface_reset_target();

            for (var i = 0; i < ds_list_size(global.terrainList); i++) { //Loop through all terrain (stored in a list to keep depth information)
                var obj = ds_list_find_value(global.terrainList, i);
                if (!instance_exists(obj)) { continue; }
                surface_set_target(rendersingle2);
                draw_clear_alpha(c_black, 0);
                draw_set_blend_mode_ext(bm_src_alpha, bm_one);
                with (obj) { //We draw the alpha mask of this "brush" to a black surface
                    draw_sprite_ext(sprite_index, 0, (x-xoffset)*(1/global.surface_scale), (y-yoffset)*(1/global.surface_scale), image_xscale*(1/global.surface_scale), image_yscale*(1/global.surface_scale), image_angle, c_white, 1);
                    visible = false;
                }
                draw_set_blend_mode(bm_normal);
                surface_reset_target();

                //Render with texture
                surface_set_target(rendersingle);
                draw_clear_alpha(c_black, 0);
                draw_background_tiled_ext(global.terrain[obj.type, TERRAIN_TEXTURE], (-xoffset)*(1/global.surface_scale), (-yoffset)*(1/global.surface_scale), 1/global.surface_scale, 1/global.surface_scale, c_white, 1);
                draw_set_blend_mode_ext(bm_inv_dest_alpha, bm_src_alpha);
                draw_surface(rendersingle2, 0, 0);
                draw_set_blend_mode(bm_normal);
                surface_reset_target();
             
                //Paste over existing terrain
                surface_set_target(rendersingle3);
                draw_set_blend_mode_ext(bm_one, bm_inv_src_alpha);
                draw_surface(rendersingle, 0, 0);
                draw_set_blend_mode(bm_normal);
                surface_reset_target();
            }
            // The code up here takes about 500ms on my PC.
            var bak = background_create_from_surface(rendersingle3, 0, 0, s, s, 0, 0); //This fellow here takes 2 seconds to complete
            tile_add(bak, 0, 0, s, s, xx*s, yy*s, 10);
        }
    }
    with (obj_terrain) {
        visible = 0;
    }
    drawn = true;
    surface_free(rendersingle);
    surface_free(rendersingle2);
    surface_free(rendersingle3);
}
After that, it runs quickly and good.

Second benchmark:
obj_render_object:
Code:
///CREATE
s = 2048; //Surface size
rendersingle = surface_create(s,s);
rendersingle2 = surface_create(s,s);
for (xx = 0; xx < ceil(room_width/s); xx++) {
    for (yy = 0; yy < ceil(room_height/s); yy++) {
        surf[xx, yy] = surface_create(s,s);
        show_debug_message('Surface made ('+string(xx)+'x'+string(yy)+'): ' + string(surf[xx, yy]));
        show_debug_message('Exists:' + string(surface_exists(surf[xx, yy]))); //They all get created and exist
    }
}

drawn = false;

///DRAW EVENT
if (drawn == false) {
    for (xx = 0; xx < ceil(room_width/s); xx++) {
        for (yy = 0; yy < ceil(room_height/s); yy++) {
            var xoffset = xx*s;
            var yoffset = yy*s;
            surface_set_target(surf[xx, yy]); //clear the tile with the background
            draw_background_tiled_ext(global.terrain[global.default_terrain, TERRAIN_TEXTURE], (-xoffset)*(1/global.surface_scale), (-yoffset)*(1/global.surface_scale), 1/global.surface_scale, 1/global.surface_scale, c_white, 1);
            surface_reset_target();

            for (var i = 0; i < ds_list_size(global.terrainList); i++) {
                var obj = ds_list_find_value(global.terrainList, i);
                if (!instance_exists(obj)) { continue; }
                //if (!collision_rectangle(xoffset, yoffset,
                surface_set_target(rendersingle2);
                draw_clear_alpha(c_black, 0);
                draw_set_blend_mode_ext(bm_src_alpha, bm_one);
                with (obj) {
                    draw_sprite_ext(sprite_index, 0, (x-xoffset)*(1/global.surface_scale), (y-yoffset)*(1/global.surface_scale), image_xscale*(1/global.surface_scale), image_yscale*(1/global.surface_scale), image_angle, c_white, 1);
                    visible = false;
                }
                draw_set_blend_mode(bm_normal);
                surface_reset_target();
             
                //Render with texture
                surface_set_target(rendersingle);
                draw_clear_alpha(c_black, 0);
                draw_background_tiled_ext(global.terrain[obj.type, TERRAIN_TEXTURE], (-xoffset)*(1/global.surface_scale), (-yoffset)*(1/global.surface_scale), 1/global.surface_scale, 1/global.surface_scale, c_white, 1);
                draw_set_blend_mode_ext(bm_inv_dest_alpha, bm_src_alpha);
                draw_surface(rendersingle2, 0, 0);
                draw_set_blend_mode(bm_normal);
                surface_reset_target();
             
                //Paste over existing terrain
                surface_set_target(surf[xx, yy]); //Draw on the surface tile
                draw_set_blend_mode_ext(bm_one, bm_inv_src_alpha);
                draw_surface(rendersingle, 0, 0);
                draw_set_blend_mode(bm_normal);
                surface_reset_target();
            }
        }
    }
    with (obj_terrain) {
        visible = 0;
    }
    drawn = true;
    surface_free(rendersingle);
    surface_free(rendersingle2);
}

for (xx = 0; xx < ceil(room_width/s); xx++) {
    for (yy = 0; yy < ceil(room_height/s); yy++) {
        draw_surface(surf[xx, yy], xx*s, yy*s); //I know there can be optimisations here - but for now this runs with 500fps.
    }
}
This runs VERY quick. Takes about 500ms to load, and then a full 500fps.

But not if my map gets too big - then my GPU messes up because it has to store a 12288x12288 32 bit bitmap... I wish GM would support me to use a surface without alpha channel (saves 1/4th the size) and maybe some lower bits / use palette's.. That would solve a lot as well.

As for the shader you're talking about... This is actually a pretty good idea! Mind sharing with me how I can accomplish such thing?

EDIT: Looks like then can only use 4 textures.. RGBA :/
 

GMWolf

aka fel666
As i said, Storing your entire map on a texture is simply the wrong way to do things.
Use shaders to dynamically draw your texture splats and blending.

There is no going around it. Just dont do it. Its stupid. (trust me. please).
 

DukeSoft

Member
I'm a donkey, I need to hit myself to a rock first before not doing it ;-)

But yeah, I'll try this approach. I'll make 2 surfaces, the size of the screen (or scaled, because they are alpha maps), and draw the masks on it (in RGBA) - this way I will have 2 textures with RGBA - so basically 8 different colours. Then I'll use a shader to draw the textures on it.

Let see how that performs!
 

DukeSoft

Member
Allrighty :)

Done it (I need some improvements in the shader when it comes to terrain blending) but the basic setup is there. Shader takes in 4 textures, and will draw the textures based on the fed "mask". It works great with high FPS on my machine - but well, my brothers laptop... Like 30 FPS. The surface method will work better for him (as long as its scaled).

I think I'll add in a few options for players;

- Surface based - Render the map first, but then have a big surface where things like blood and bullet shells can be drawn on to. Takes up a lot of GRAM, but in the end you have a nice effect. Good for high-end GPU's
- Shader based - Renders the map on the fly. Requires a medium-range GPU. Particles will be drawn by particle manager and will take up some more CPU. Uses very little GRAM.
- Tile based - Uses tilebased setup for terrain. Looks terrible, but works for low-end GPU and CPU's.
 

GMWolf

aka fel666
well, my brothers laptop... Like 30 FPS.
Im pretty sure you are doing something ineficient with your shader... Here i easily get hundreds of FPS on and hd 4000. Runs on phone at 60. (android caps it.).
quite fast on my 860M, naturally.

Rememebr to only render what's on screen.
 

DukeSoft

Member
Im pretty sure you are doing something ineficient with your shader... Here i easily get hundreds of FPS on and hd 4000. Runs on phone at 60. (android caps it.).
quite fast on my 860M, naturally.

Rememebr to only render what's on screen.
I must say that I'm using the shader 3 times. twice for the 8 terrain textures I have, and once for the walls (should be a higher depth - and is a different texture).

Here's my shader;
Fragment
Code:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texR; //Texture for R
uniform sampler2D texG; //Texture for R
uniform sampler2D texB; //Texture for R
uniform sampler2D texA; //Texture for R

uniform float surfWidth; 
uniform float surfHeight; 

uniform float xpos;
uniform float ypos;

void main()
{
    vec2 texpos = (v_vTexcoord*vec2(surfWidth, surfHeight)) + vec2(xpos, ypos);
   
    float tr = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).r;
    float tg = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).g;
    float tb = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).b;
    float ta = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).a;
   
    vec4 ter1 = texture2D(texR, texpos);
    ter1.a = tr;
    vec4 ter2 = texture2D(texG, texpos);
    ter2.a = tg;
    vec4 ter3 = texture2D(texB, texpos);
    ter3.a = tb;
    vec4 ter4 = texture2D(texA, texpos);
    ter4.a = ta;
   
    gl_FragColor = ter1*vec4(tr) + ter2*vec4(tg) + ter3*vec4(tb) + ter4*vec4(ta); // There is much to be improved here
    //gl_FragColor = texture2D( texG, v_vTexcoord*vec2(surfSize) );//v_vColour * texture2D( texG, v_vTexcoord );
    //gl_FragColor.a = 1.0;
    //gl_FragColor = vec4(v_vTexcoord.x,v_vTexcoord.y,1,1);
}
This is my "terraindrawer":
Create
Code:
surface_width = global.wview;
surface_height = global.hview;
surface_scale = 1;
texsize = 256; //Actual texture is 512x512 - but I use this to make it look less pixely when the game is zoomed in (i have a few vector images in there as well)
terrainsurface1 = surface_create(surface_width/surface_scale, surface_height/surface_scale);
terrainsurface2 = surface_create(surface_width/surface_scale, surface_height/surface_scale);
wallsurface = surface_create(surface_width/surface_scale, surface_height/surface_scale);

ts_width = shader_get_uniform(shdr_terrain,"surfWidth");
ts_height = shader_get_uniform(shdr_terrain,"surfHeight");

ts_x = shader_get_uniform(shdr_terrain,"xpos");
ts_y = shader_get_uniform(shdr_terrain,"ypos");

ts_r = shader_get_sampler_index(shdr_terrain,"texR");
ts_g = shader_get_sampler_index(shdr_terrain,"texG");
ts_b = shader_get_sampler_index(shdr_terrain,"texB");
ts_a = shader_get_sampler_index(shdr_terrain,"texA");

texture_set_repeat_ext(ts_r, true);
texture_set_repeat_ext(ts_g, true);
texture_set_repeat_ext(ts_b, true);
texture_set_repeat_ext(ts_a, true);

baktex1 = background_get_texture(global.terrain[1, TERRAIN_TEXTURE]);
baktex2 = background_get_texture(global.terrain[2, TERRAIN_TEXTURE]);
baktex3 = background_get_texture(global.terrain[3, TERRAIN_TEXTURE]);
baktex4 = background_get_texture(global.terrain[4, TERRAIN_TEXTURE]);

baktex5 = background_get_texture(global.terrain[5, TERRAIN_TEXTURE]);
baktex6 = background_get_texture(global.terrain[7, TERRAIN_TEXTURE]);
baktex7 = background_get_texture(global.terrain[8, TERRAIN_TEXTURE]);
baktex8 = background_get_texture(global.terrain[9, TERRAIN_TEXTURE]);

walltex = background_get_texture(bak_concrete);
Draw begin
Code:
surface_set_target(terrainsurface1);
    draw_clear_alpha(c_black, 0);
    draw_set_colour_write_enable(1, 0, 0, 0);
    drawTerObj(1, surface_scale);
    draw_set_colour_write_enable(0, 1, 0, 0);
    drawTerObj(2, surface_scale);
    draw_set_colour_write_enable(0, 0, 1, 0);
    drawTerObj(3, surface_scale);
    draw_set_blend_mode(bm_add);
    draw_set_colour_write_enable(0, 0, 0, 1);
    drawTerObj(4, surface_scale);
    draw_set_colour_write_enable(1, 1, 1, 1);
    draw_set_blend_mode(bm_normal);
surface_reset_target();

surface_set_target(terrainsurface2);
    draw_clear_alpha(c_black, 0);
       
    draw_set_colour_write_enable(1, 0, 0, 0);
    drawTerObj(5, surface_scale);
    draw_set_colour_write_enable(0, 1, 0, 0);
    drawTerObj(7, surface_scale);
    draw_set_colour_write_enable(0, 0, 1, 0);
    drawTerObj(8, surface_scale);
    draw_set_blend_mode(bm_add);
    draw_set_colour_write_enable(0, 0, 0, 1);
    drawTerObj(9, surface_scale);
    draw_set_colour_write_enable(1, 1, 1, 1);
    draw_set_blend_mode(bm_normal);
surface_reset_target();

surface_set_target(wallsurface);
    draw_clear_alpha(c_black, 0);
    drawWallObj(surface_scale);
surface_reset_target();
Draw event
Code:
if (!surface_exists(terrainsurface1)) {
    terrainsurface1 = surface_create(surface_width/surface_scale, surface_height/surface_scale);
} 
if (!surface_exists(terrainsurface2)) {
    terrainsurface2 = surface_create(surface_width/surface_scale, surface_height/surface_scale);
}
if (!surface_exists(wallsurface)) {
    wallsurface = surface_create(surface_width/surface_scale, surface_height/surface_scale);
}

draw_background_tiled(global.terrain[global.default_terrain, TERRAIN_TEXTURE], 0, 0);

///Terrainset 1
shader_set(shdr_terrain)
texture_set_stage(ts_r,baktex1);
texture_set_stage(ts_g,baktex2);
texture_set_stage(ts_b,baktex3);
texture_set_stage(ts_a,baktex4);
shader_set_uniform_f(ts_width, surface_width/texsize);
shader_set_uniform_f(ts_height, surface_height/texsize);
shader_set_uniform_f(ts_x, view_xview/texsize);
shader_set_uniform_f(ts_y, view_yview/texsize);
draw_surface_stretched(terrainsurface1,view_xview,view_yview, surface_width, surface_height);
shader_reset()

//Terrain set 2
shader_set(shdr_terrain)
texture_set_stage(ts_r,baktex5);
texture_set_stage(ts_g,baktex6);
texture_set_stage(ts_b,baktex7);
texture_set_stage(ts_a,baktex8);
shader_set_uniform_f(ts_width, surface_width/texsize);
shader_set_uniform_f(ts_height, surface_height/texsize);
shader_set_uniform_f(ts_x, view_xview/texsize);
shader_set_uniform_f(ts_y, view_yview/texsize);
draw_surface_stretched(terrainsurface2,view_xview,view_yview, surface_width, surface_height);
shader_reset()

//Concrete walls
shader_set(shdr_terrain)
texture_set_stage(ts_a,walltex);
shader_set_uniform_f(ts_width, surface_width/texsize);
shader_set_uniform_f(ts_height, surface_height/texsize);
shader_set_uniform_f(ts_x, view_xview/texsize);
shader_set_uniform_f(ts_y, view_yview/texsize);
draw_surface_stretched(wallsurface,view_xview,view_yview, surface_width, surface_height);
shader_reset()
drawTerObj
Code:
///drawTerObj(type, scale);
var spacer = 128;
with (obj_terrain) {
     if (type == argument0 && collision_rectangle(view_xview-spacer, view_yview-spacer, view_xview+view_wview+spacer, view_yview+view_hview+spacer, id, 0, 0)) {
         draw_sprite_ext(sprite_index, 1, (x-view_xview)/argument1, (y-view_yview)/argument1, image_xscale/argument1, image_yscale/argument1, image_angle, c_white, 1);
     }
}
drawWallObj
Code:
///drawWallObj(scale);
var spacer = 128;
with (obj_wall_concrete) {
     if (collision_rectangle(view_xview-spacer, view_yview-spacer, view_xview+view_wview+spacer, view_yview+view_hview+spacer, id, 0, 0)) {
         draw_sprite_ext(sprite_index, image_index, (x-view_xview)/argument0, (y-view_yview)/argument0, image_xscale/argument0, image_yscale/argument0, image_angle, c_white, 1);
     }
}
As you can see I still have a few unresolved issues;
1. My shader works good when there's single terrain objects. When they overlay, their textures will overlay, and the RGB values are being combined - they will start to look "brighter" on those places
2. Layering doesn't work anymore. When I create a block of sand, overlay it with grass, and then overlay that with a square block of stones - the shader draws whatever colours in there. I think I'll have to edit some code in the piece thats drawing to the surface.. Something with blend modes - I'm not sure where to start
3. Its still not that fast. Allthough, my brothers laptop runs about 120fps when I'm using a single shader (4 terrains).
 

DukeSoft

Member


Here's the result of 1 shader.
Left bottom is the surface without a shader. (Please note that you won't see the alpha channel when there's no RGB data in it ofcourse :) )

Also - @icuurd12b42 fixed it pretty neatly in his marketplace asset; https://marketplace.yoyogames.com/assets/2143/tmc-terrain-tile

He also uses just 1 channel for the terrains... Wonder how he did it - I'm sure that method is much faster (also given the fact he uses 1 texture with all terrains on them, but tiled)
 

GMWolf

aka fel666
You can compress any number of channels into a single image if you want, at the cost of accuracy. (its a bit of complicated math, but it works).
Also, since your walls have a single texture, draw them outside of the surface. Just draw them to screen after the terrain directly, using a shader that will map the texture based on world coordinates rather than object coordinates.

You should also consider putting all your textures on the same map, this way your shader is only having to draw a single texture. Much more efficient that way, as you reduce all texture swaps 8 fold.
 

DukeSoft

Member
You can compress any number of channels into a single image if you want, at the cost of accuracy. (its a bit of complicated math, but it works).
Also, since your walls have a single texture, draw them outside of the surface. Just draw them to screen after the terrain directly, using a shader that will map the texture based on world coordinates rather than object coordinates.

You should also consider putting all your textures on the same map, this way your shader is only having to draw a single texture. Much more efficient that way, as you reduce all texture swaps 8 fold.
Ah, ofcourse.. I could use the R channel from 0-127 for tex1, 128-255 for tex2.

Neat, I'll try that.

Do you have any idea how i would fix the "lighting" issue when textures overlap? I think I'll have to make sure that R+G+B+A never is higher than 255... But i have no idea how I could accomplish that :( I'm guessing this can be done in the shader efficiently, but I have very little experience with shaders..
 

DukeSoft

Member
So, I now use 1 shader pass with 1 texture - and this allows me to have 8 "terrains" in 1 pass. I'll have to do some editing on the drawing on the surface, but the shader works. Any ideas how I could speed up this shader, making it more efficient?

Output example:
Every ball is a test (RGBA channel from left to right, intensity from 63, 126, 196, 255 (so 0.25, 0.5, 0.75, 1.0))

Terrain texture:

Fragment Shader:
Code:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texture; //Texture

uniform float surfWidth;
uniform float surfHeight;

uniform float xpos;
uniform float ypos;

void main()
{
    //Setting the main texture position
    vec2 texpos = (v_vTexcoord*vec2(surfWidth, surfHeight)) + vec2(xpos, ypos);

    //Modulo 1 for repeat, devide by 4 because its a 4x4 texture sheet
    float tpx = mod(texpos.x,1.0)/4.0;
    float tpy = mod(texpos.y,1.0)/4.0;

    //Setup terrain "intensities"
    float t1 = 0.0;
    float t2 = 0.0;
    float t3 = 0.0;
    float t4 = 0.0;
    float t5 = 0.0;
    float t6 = 0.0;
    float t7 = 0.0;
    float t8 = 0.0;

    //Load from the surface (splatmap)
    float btr = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).r;
    float btg = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).g;
    float btb = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).b;
    float bta = (v_vColour * texture2D(gm_BaseTexture, v_vTexcoord)).a;
  
    //Calculating what texture to use (also deviding each channel into 2)
    if (btr <= 0.5) {
        t1 = btr*2.0;
    } else {
        t5 = (btr-0.5)*2.0;
    }
  
    if (btg <= 0.5) {
        t2 = btg*2.0;
    } else {
        t6 = (btg-0.5)*2.0;
    }
  
    if (btb <= 0.5) {
        t3 = btb*2.0;
    } else {
        t7 = (btb-0.5)*2.0;
    }
  
    if (bta <= 0.5) {
        t4 = bta*2.0;
    } else {
        t8 = (bta-0.5)*2.0;
    }

    //Get terrain pixels at proper positions
    vec4 ter1 = texture2D(texture, vec2(tpx, tpy));
    ter1.a = t1;
    vec4 ter2 = texture2D(texture, vec2(tpx+0.25, tpy));
    ter2.a = t2;
    vec4 ter3 = texture2D(texture, vec2(tpx+0.50, tpy));
    ter3.a = t3;
    vec4 ter4 = texture2D(texture, vec2(tpx+0.75, tpy));
    ter4.a = t4;
   
    vec4 ter5 = texture2D(texture, vec2(tpx, tpy+0.25));
    ter5.a = t5;
    vec4 ter6 = texture2D(texture, vec2(tpx+0.25, tpy+0.25));
    ter6.a = t6;
    vec4 ter7 = texture2D(texture, vec2(tpx+0.50, tpy+0.25));
    ter7.a = t7;
    vec4 ter8 = texture2D(texture, vec2(tpx+0.75, tpy+0.25));
    ter8.a = t8;

    //Output to screen
    gl_FragColor =
    ter1*vec4(t1) +
    ter2*vec4(t2) +
    ter3*vec4(t3) +
    ter4*vec4(t4) +
    ter5*vec4(t5) +
    ter6*vec4(t6) +
    ter7*vec4(t7) +
    ter8*vec4(t8);
}

I feel like this shader is terribly inefficient, and would love to know if someone knows how to speed it up.
 
Last edited:

GMWolf

aka fel666
Yeah. Try to never use if statements in shaders. They are horrendously slow!
Multiply your results by 0 instead...

I think your method of drawing the mask may also be a little inefficient.
 

DukeSoft

Member
Yeah. Try to never use if statements in shaders. They are horrendously slow!
Multiply your results by 0 instead...

I think your method of drawing the mask may also be a little inefficient.
Hey Fel,

Thanks, didn't know that :) How do you mean, multiply results by 0? Because R0.4999 is the max for texture one, where R0.5 is the minimum for texture 5 (and R1.0 is the max for texture 5).
 

Juju

Member
Edit: Just realised that this only provides either 1 or 5, 2 or 6 etc. so you may want to ignore this :p

Code:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D texture; //Texture

uniform float surfWidth;
uniform float surfHeight;
uniform float xpos;
uniform float ypos;

void main()
{
  
    //Setting the main texture position
    vec2 texpos = v_vTexcoord * vec2( surfWidth, surfHeight ) + vec2(xpos, ypos);

    //Modulo 1 for repeat, devide by 4 because its a 4x4 texture sheet
    float tpx = 0.25 * mod( texpos.x, 1.0 );
    float tpy = 0.25 * mod( texpos.y, 1.0 );
  
    //Collect splat data
    vec4 vSplatSample   = texture2D( gm_BaseTexture, v_vTexcoord );
    vec4 vSplatPosition = 0.25 * floor( 2.0 * vSplatSample );
    vec4 vSplatAlpha    = mod( 2.0 * vSplatSample, 1.0 );
  
    //Sample splat textures
    vec4 vSample = vec4( 0.0 );
    if ( vSplatAlpha.r > 0.0 ) vSample = mix( vSample, texture2D( texture, vec2( tpx       , tpy + vSplatPosition.r ) ), vSplatAlpha.r );
    if ( vSplatAlpha.g > 0.0 ) vSample = mix( vSample, texture2D( texture, vec2( tpx + 0.25, tpy + vSplatPosition.g ) ), vSplatAlpha.g );
    if ( vSplatAlpha.b > 0.0 ) vSample = mix( vSample, texture2D( texture, vec2( tpx + 0.50, tpy + vSplatPosition.b ) ), vSplatAlpha.b );
    if ( vSplatAlpha.a > 0.0 ) vSample = mix( vSample, texture2D( texture, vec2( tpx + 0.75, tpy + vSplatPosition.a ) ), vSplatAlpha.a );
    gl_FragColor = v_vColour * vSample;
  
}
I'm presuming a texture lookup is more expensive than a branch. If it's not, drop the if-statements.
 

DukeSoft

Member
Nah, the if statements are terrible on most GPU's because the pipeline has to be rebuilt.
If i'm not mistaken, the shader writes the calculations per pixel into a hardware chip / pipeline on the GPU, and then this gets fed with every pixel information - thats why its blazingly fast. When branching off using if's the "code" must be rewritten into the GPU. (Don't pin me on this! This is what i'm thinking)

Also, a bit of example code would be awesome :) As soon as I get my account on shadertoy setup I'll put the shader + demo there :)
 

GMWolf

aka fel666
Well I don't know how much time to I'll get.
Ill try but I can't promise anything.
If a shader guru could put something together that would be great! Anyone?
 

DukeSoft

Member
Thanks a lot for your help so far @Fel666 :)

This is the solution I took;

Terrain layer (map editor can choose a default terrain, and 4 extra terrains)
Floor layer (on top of terrain) (can choose 4 floor types)
Wall layer (can chose 4 wall types)

The terrain and floor layer work the same. RGBA for 1 of the 4 terrain types onto a shader. No data = no terrain.

The terrain renderer creates 1 texture for every layer - this one consist out of 4 different textures (its made on the fly). This way the player can also have a texture "resolution".

Same for the floors, and walls.

Result:


Shadows and lighting have yet to be added :) but for now this works on a decent FPS.

Cheers!
 
Top