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

SOLVED Applying this Wave Shader To A Tile Layer



So I've got heat wave / retro underwater effect shader. You can see it applied to a sprite above. ^^^^
However, I'm trying to apply to a entire tile layer and the effect is...right not quite.



As you can see, it seems to be applying the effect to each tile chunk separately, instead of the whole layer.

Here's the code for reference. This is the step event of a controller object:

GML:
var lay_id = layer_get_id("Tiles");
layer_script_begin(lay_id, scr_tile_wave_fx);
layer_script_end(lay_id, scr_tile_shader_reset);

That script, scr_tile_wave_fx, goes as follows:


GML:
if event_type == ev_draw
   {
    if  event_number == 0
      {
        //Create event:
        uTime = floor(shader_get_uniform(shd_wave, "Time"));
        uTexel = floor(shader_get_uniform(shd_wave, "Texel"));

        //Draw event:
        shader_set(shd_wave);
        shader_set_uniform_f(uTime, current_time);
        shader_set_uniform_f(uTexel, 0.0025, 0.005);
      }
   }


And if really curious, here's the fragment shader I picked up from nacho_chicken for the wave effect:
Code:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform float Time;
uniform vec2 Texel;

//Modify the following three consts to change the wave effect to your liking
const float xSpeed = 0.005;
const float xFreq = 175.0;
const float xSize = 1.0;

void main()
{
    float xWave = sin(Time*xSpeed + v_vTexcoord.y*xFreq) * (xSize*Texel.x);
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord + vec2(xWave, 0.0));
}

So, any ideas how to get this shader to apply to the tile layer as a whole and not each separate tile cell?
 

NightFrost

Member
You can use draw_tilemap to easily draw your tiles to target surface. Set the actual tilemap layer not visible after done building the level, or programmatically at runtime. Have a controller object on a layer at near-equal depth (so draw order is preserved) to create a surface of view size at room start. Have it draw the tilemap to the surface with negative offsets equal to camera view position (this offsets into correct spot on the tilemap). Have it then draw the surface to application surface using the distort shader. Have it destroy the surface at cleanup.
 
You can use draw_tilemap to easily draw your tiles to target surface. Set the actual tilemap layer not visible after done building the level, or programmatically at runtime. Have a controller object on a layer at near-equal depth (so draw order is preserved) to create a surface of view size at room start. Have it draw the tilemap to the surface with negative offsets equal to camera view position (this offsets into correct spot on the tilemap). Have it then draw the surface to application surface using the distort shader. Have it destroy the surface at cleanup.
Good heavens, this such a messy ordeal just to get the shader to a layer. I'll try it, but goodness does GM need a simpler way then to be effin' around with surfaces.
 

GMWolf

aka fel666
The way I would do it is not with draw_tilemap, but with layer_script_begin/end like in your original post.

Rather than applying the shader directly though, I would set the surface target in layer_script_begin, and then reset and draw that surface using the shader in layer script end.
 

NightFrost

Member
Good heavens, this such a messy ordeal just to get the shader to a layer. I'll try it, but goodness does GM need a simpler way then to be effin' around with surfaces.
Yeah it becomes bit on an ordeal because each tile is a separate draw call, so the shader gets applied to that draw only, each time. When an effect needs to treat the entire tilemap as a single entity instead, it becomes necessary to draw it in whole before applying the effect. It would be nice if there was an event in draw event chain that had the entire layer content available as target. But I assume the way GMS draws is it doesn't do any sort of intermediate steps for speed, and always applies a draw directly to application surface. So you cannot target "complete layer content" because GMS doesn't create one.
 
You can use draw_tilemap to easily draw your tiles to target surface. Set the actual tilemap layer not visible after done building the level, or programmatically at runtime. Have a controller object on a layer at near-equal depth (so draw order is preserved) to create a surface of view size at room start. Have it draw the tilemap to the surface with negative offsets equal to camera view position (this offsets into correct spot on the tilemap). Have it then draw the surface to application surface using the distort shader. Have it destroy the surface at cleanup.
GML:
if surface_exists(WaveSurface)
{
    surface_set_target(WaveSurface);

        
        var c = view_camera[0];
        var cx = camera_get_view_x(c);
        var cw = camera_get_view_width(c)
        var cy = camera_get_view_y(c);
        var ch = camera_get_view_height(c)
    
        var lay_id = layer_get_id("TilesSPZ");

        var map_id = layer_tilemap_get_id(lay_id);
        draw_tilemap(map_id,0,0)
        
        layer_set_visible(lay_id,false);
        
        surface_reset_target();
        
        draw_surface(WaveSurface,0,0);
}
What am I missing here? Can't get the tile layer to draw... I see nothing right now...
 
Rather than applying the shader directly though, I would set the surface target in layer_script_begin, and then reset and draw that surface using the shader in layer script end.
@Noah Copeland You mean you haven't tried this yet? You PM'd me about this the other day, and in my first response I recommended to try this method first. I tried it myself; it was super simple and worked perfectly the first time:

1594670625922.png

GML:
// This can go in any object or room's creation code
global.__waveShader = shd_wave;
global.__uTime = shader_get_uniform(global.__waveShader, "Time");
global.__uTexel = shader_get_uniform(global.__waveShader, "Texel");

var tileLayer = layer_get_id("TilesMain");
global.__layerSurf = -1;

layer_script_begin(tileLayer, tile_layer_wave_begin);
layer_script_end(tileLayer, tile_layer_wave_end);
GML:
// tile_layer_wave_begin()
if (event_type == ev_draw) {
  if (event_number == 0) {
    if (!surface_exists(global.__layerSurf)) {
      global.__layerSurf = surface_create(512, 512);
    }

    surface_set_target(global.__layerSurf);
    draw_clear_alpha(0xFFFFFF, 0);
  }
}
GML:
// tile_layer_wave_end()
if (event_type == ev_draw) {
  if (event_number == 0) {
    surface_reset_target();

    shader_set(global.__waveShader);
      shader_set_uniform_f(global.__uTime, current_time);
      var tex = surface_get_texture(global.__layerSurf);
      shader_set_uniform_f(global.__uTexel, texture_get_texel_width(tex), texture_get_texel_height(tex));
      draw_surface(global.__layerSurf, 0, 0);
    shader_reset();
  }
}
Note: Make sure to set the tile layer to visible. These scripts make sure all the draw code is targeted to a surface, so nothing will be visible until the surface itself is drawn.
 
Last edited:
  • Like
Reactions: Yal

GMWolf

aka fel666
one caveat with views: you will want to use a surface that is slightly larger than your view, and a camera that is slightly larger than the view too. then draw the surface with the original camera.
That is in order to render the border around the view and avoid clipping with the wave shader as it samples outside the view.

I made a video tutorial involving setting surfaces and cameras for individual layers (in the context of a lighting system). I remember views being a a little weird when setting surfaces and exaplained how to resolve it in the tutorial.
 
@Noah Copeland You mean you haven't tried this yet? You PM'd me about this the other day, and in my first response I recommended to try this method first. I tried it myself; it was super simple and worked perfectly the first time:

View attachment 32733

GML:
// This can go in any object or room's creation code
global.__waveShader = shd_wave;
global.__uTime = shader_get_uniform(global.__waveShader, "Time");
global.__uTexel = shader_get_uniform(global.__waveShader, "Texel");

var tileLayer = layer_get_id("TilesMain");
global.__layerSurf = -1;

layer_script_begin(tileLayer, tile_layer_wave_begin);
layer_script_end(tileLayer, tile_layer_wave_end);
GML:
// tile_layer_wave_begin()
if (event_type == ev_draw) {
  if (event_number == 0) {
    if (!surface_exists(global.__layerSurf)) {
      global.__layerSurf = surface_create(512, 512);
    }

    surface_set_target(global.__layerSurf);
    draw_clear_alpha(0xFFFFFF, 0);
  }
}
GML:
// tile_layer_wave_end()
if (event_type == ev_draw) {
  if (event_number == 0) {
    surface_reset_target();

    shader_set(global.__waveShader);
      shader_set_uniform_f(global.__uTime, current_time);
      var tex = surface_get_texture(global.__layerSurf);
      shader_set_uniform_f(global.__uTexel, texture_get_texel_width(tex), texture_get_texel_height(tex));
      draw_surface(global.__layerSurf, 0, 0);
    shader_reset();
  }
}
Note: Make sure to set the tile layer to visible. These scripts make sure all the draw code is targeted to a surface, so nothing will be visible until the surface itself is drawn.
I had not tried because at the time, 1) You expressed unfamiliar with GMS2's layer system and took what appeared to be an educated stab at it 2) I was hoping for an elegant solution that didn't involve surfaces; I now know there is none.Together this resulted in me trying other methods first.

However, as usually with programming, the thing I've been avoiding is the thing I gotta suck up and do.

I merged it into my code and after some fiddling have got it working in the upper left corner of the room. However, I'm struggling to see how this works beyond a static screen, when it needs to work with a moving camera. As mentioned by GMWolf, the surface will need to be bigger than the view, but it adapts dynamically as the camera's coordinates move around the room is unclear to me at this point.
 

GMWolf

aka fel666
I had not tried because at the time, 1) You expressed unfamiliar with GMS2's layer system and took what appeared to be an educated stab at it 2) I was hoping for an elegant solution that didn't involve surfaces; I now know there is none.Together this resulted in me trying other methods first.

However, as usually with programming, the thing I've been avoiding is the thing I gotta suck up and do.

I merged it into my code and after some fiddling have got it working in the upper left corner of the room. However, I'm struggling to see how this works beyond a static screen, when it needs to work with a moving camera. As mentioned by GMWolf, the surface will need to be bigger than the view, but it adapts dynamically as the camera's coordinates move around the room is unclear to me at this point.
check out the link in my previous post. its a tutorial i made a while back, it solves the camera problem.

also, yeah, no way around not using a surface.
When running the distortion shader, you are sampling from outised individual tiles (or chunks). they cannot know what is outside their chunk as it hasnt been rendered yet. So instead you need to render to a surface so that what is around a tile/chunk can be sampled.
 
check out the link in my previous post. its a tutorial i made a while back, it solves the camera problem.

also, yeah, no way around not using a surface.
When running the distortion shader, you are sampling from outised individual tiles (or chunks). they cannot know what is outside their chunk as it hasnt been rendered yet. So instead you need to render to a surface so that what is around a tile/chunk can be sampled.
Yes, I had already watched your video but I'm failing to see how this applies to the current situation. 🤔
I've thrown it in anyway and the results are...well.... not sure what is even happening here 😅
2020-07-13_16-55-31.gif


for more context, without your "camera_apply()"; stuff, the effects looks as expected, but stops after the surface's size of 512 has been exceeded by the camera/player. See gif below.

 

GMWolf

aka fel666
yeah, thats because the surface is only occupying the top left corner of the room, and the camera isnt being applied to it.
The code in the tutorial is supposed to apply the camera to fix this.
ALthough i think at some point GM changed a little and broke it. i think the trick to reset the camera by setting the surface doesnt work anymore, you might need to create a view instead.
 
yeah, thats because the surface is only occupying the top left corner of the room, and the camera isnt being applied to it.
The code in the tutorial is supposed to apply the camera to fix this.
ALthough i think at some point GM changed a little and broke it. i think the trick to reset the camera by setting the surface doesnt work anymore, you might need to create a view instead.
None of that quite makes sense. Why does it draw the surface scaled like it did in the gif in the previous post? I also tried the fix listed in your topic to this lightning system (to accommodate for the GM update) and things get reaaalll whacky.


I'm not following the through line of any of this. I need to be capturing the section of the tile map that is currently on screen and then drawing it via surface at camera_get_view_x and camera_get_view_y, but how can I capture it accurately?
 

Nocturne

Friendly Tyrant
Forum Staff
Admin
You PM'd me about this the other day
This caught my attention.... Slightly off topic, but it needs said, just in case. You should never PM a member unless they explicitly state that you can contact them in a topic or a status update. Especially not to ask for help. Not only is it rude to the person you are asking as it puts them on the spot, it's incredibly selfish as a topic may benefit everyone on the forum while a PM only helps the person that sent it. I would also say that if anyone receives unsolicited PMs then they should report them and not respond.

PS: If this is not the case, then please ignore the PSA post. :p
 

GMWolf

aka fel666
Check this post and topic:
 
Check this post and topic:
That is the post I was referring to previously. I tried fiddling around these methods. The results are what you see in the gif I posted earlier. I'll post it again for clarity.
 
Okay, idk what happened but when I ran the game today.... it worked. I don't remember changing anything, but I must have last minute and not tested it.



I'm not super happy with the way the effect seems to "speed up" as travel vertically....but at least it appears to be working finally

here's the code for anyone else trying this, called through the layer_scripts seen earlier

GML:
//tile_layer_wave_begin

if event_type == ev_draw
   {
   if event_number == 0
      {


                var c = view_camera[0];
              
                if (!surface_exists(global.__layerSurf)) {
                  global.__layerSurf = surface_create(320, 224);
                }

                surface_set_target(global.__layerSurf);
              
              
                camera_apply(c);
              
                draw_clear_alpha(0xFFFFFF, 0);


      }
   }


GML:
//tile_layer_wave_end

if event_type == ev_draw
   {
   if event_number == 0
      {
        
        var c = view_camera[0];
        var cx = camera_get_view_x(c);
        var cy = camera_get_view_y(c);
  
  
        surface_reset_target();
      
  
      
        shader_set(global.__waveShader);
        shader_set_uniform_f(global.__uTime, current_time);
        var tex = surface_get_texture(global.__layerSurf);
        shader_set_uniform_f(global.__uTexel, texture_get_texel_width(tex), texture_get_texel_height(tex));
        draw_surface(global.__layerSurf, cx, cy);
        shader_reset();

        
        
      }
   }
 

GMWolf

aka fel666
I'm not super happy with the way the effect seems to "speed up" as travel vertically....but at least it appears to be working finally
You might need to offset the animation time based on the camera y position. Not sure what the exact maths would be...
 
Last edited:

Nocturne

Friendly Tyrant
Forum Staff
Admin
Okay, idk what happened but when I ran the game today.... it worked. I don't remember changing anything, but I must have last minute and not tested it.
This could have been a caching issue. GMS2 caches assets and this can sometimes lead to odd issues, especially with the graphics. On restarting the IDE and loading the project it probably cleared the cache and that's why it's working. Generally, whenever something doesn't seem to look right or is "weird" then I always clear the cache first before doing anything else (from the "broom" icon at the top of the IDE.
 

GMWolf

aka fel666
Okay, idk what happened but when I ran the game today.... it worked. I don't remember changing anything, but I must have last minute and not tested it.



I'm not super happy with the way the effect seems to "speed up" as travel vertically....but at least it appears to be working finally

here's the code for anyone else trying this, called through the layer_scripts seen earlier

GML:
//tile_layer_wave_begin

if event_type == ev_draw
   {
   if event_number == 0
      {


                var c = view_camera[0];
             
                if (!surface_exists(global.__layerSurf)) {
                  global.__layerSurf = surface_create(320, 224);
                }

                surface_set_target(global.__layerSurf);
             
             
                camera_apply(c);
             
                draw_clear_alpha(0xFFFFFF, 0);


      }
   }


GML:
//tile_layer_wave_end

if event_type == ev_draw
   {
   if event_number == 0
      {
       
        var c = view_camera[0];
        var cx = camera_get_view_x(c);
        var cy = camera_get_view_y(c);
 
 
        surface_reset_target();
     
 
     
        shader_set(global.__waveShader);
        shader_set_uniform_f(global.__uTime, current_time);
        var tex = surface_get_texture(global.__layerSurf);
        shader_set_uniform_f(global.__uTexel, texture_get_texel_width(tex), texture_get_texel_height(tex));
        draw_surface(global.__layerSurf, cx, cy);
        shader_reset();

       
       
      }
   }
To save you some future pain: rather than drawing the surface at the view position, try not having a view at all.
You could do that, for example, by resetting the view matrix, or by setting a camera with default position, scale and rotation.
This will save you some trouble when scaling/rotating the camera down the line.
 
To save you some future pain: rather than drawing the surface at the view position, try not having a view at all.
You could do that, for example, by resetting the view matrix, or by setting a camera with default position, scale and rotation.
This will save you some trouble when scaling/rotating the camera down the line.
I don't understand what you are talking about. What do you mean not having a view at all? We are definitely gonna be needing a view, this game isn't staying still on one screen, and will def be moving around.
There won't any scaling or rotating of the camera either, as that's more of a SNES thing, and I'm going for Genesis hardware vibes here.

As far as setting animation time off by the camera height, that's sounds a little more like a through-line... Do you think that'd be within the shader itself? 🤔
 

GMWolf

aka fel666
I don't understand what you are talking about. What do you mean not having a view at all? We are definitely gonna be needing a view, this game isn't staying still on one screen, and will def be moving around.
There won't any scaling or rotating of the camera either, as that's more of a SNES thing, and I'm going for Genesis hardware vibes here.
What I mean is when drawing the surface in layer_end. Not when drawing objects etc. Just when drawing that surface.
At the moment, you have a camera into the world, and then drawing the surface at that position.
It's a little weird.
It would probably better to reset the view when drawing the surface. That way you just draw the surface at 0,0 and all the view position, translation, rotation is only applied once (when drawing to the surface) instead of also being applied when drawing the surface.
I think you can just disable the camera I'm not sure.


As far as setting animation time off by the camera height, that's sounds a little more like a through-line... Do you think that'd be within the shader itself? 🤔
You could supply another uniform for camera position to the shader. Then apply that offset to the input of the wave function.
There won't any scaling or rotating of the camera either, as that's more of a SNES thing, and I'm going for Genesis hardware vibes her
Fair enough. It's just "more correct" to not be applying a camera to a post process surface. At that point you are no longer working in world space, but screen space.

[Edit] I totally get the whole "why change it if it's working" thing but I think it's worth doing things 'correctly'. It helps understand what's really going on, and Ultimately makes life easier down the line if you ever need to change things. (Of course when on a deadline a trade-off needs to be made)
 
Found a much more elegant solution to keep the vertical speed consistent. You gotta establish an offset variable in the shader

"uniform float Offset;"

and then add that to v_vTexcoord.y, in this line here:
GML:
    float xWave = sin(Time*xSpeed + (v_vTexcoord.y+Offset)*xFreq) * (xSize*Texel.x);

Then in the wave_effect_end script, just set the Offset to camera's y position divided by the screen height (which is a Genesis-accurate 224px in this case)

Code:
shader_set_uniform_f(global.__uOffset, cy/224);
And, in the gif below you even see that you can use draw_surface_part to use it as an underwater effect below the water line, like so
Code:
draw_surface_part(global.__layerSurf,0,waterY-cy,320,224,cx,waterY);

 
Top