Trouble with ping ponging surfaces when using transparency....[solved, kind of]

EDITED IN A TLDR AT THE BOTTOM OF THE POST. I actually figured out a way to get all of this working, but I don't understand why - picture of what I changed is at the bottom, but I'd still love if someone could explain to me why the code is working now, because it seems like my old code should've worked, too. Thank you to anyone with an answer or ideas!

Hey guys! Having some trouble with surfaces. I have a basic set up in my "DrawScreen" object's End Draw event:

Code:
//All Draw scripts here reset target shader and surface when they're finished running:
DrawScreen_DrawGameField(); //Copies base game from app surface to surfFinishedScreen in correct palette chosen in currentShader.
DrawScreen_DrawLighting(); //Copies surfLighting onto surfFinishedScreen.
DrawScreen_PostProcessing2();
If I comment out DrawScreen_DrawLighting() or DrawScreen_PostProcessing2(), things work as intended. For whatever reason, they just aren't playing well together! DrawLighting draws copies a transparent surface to my main screen surface to darken the room. PostProcessing2 just has a shader running that draws everything to the left (just for testing purposes). Here are how things look with:

JUST LIGHTING -behaving as expected!


JUST POST PROCESSING - behaving as expected, too!


LIGHTING AND PROCESSING - something's wrong. We've got a weird ghost image going on!


I don't get how this is happening, because in Lighting, I just draw the lighting onto the Screen surface. In post processing, I do some ping ponging to rewrite that screen surface. Maybe I'm doing it wrong? 😅
Here's the code for the three functions called up there:

Code:
function DrawScreen_DrawGameField() //Copies base game from app surface to surfFinishedScreen in correct palette chosen in currentShader. ======================================================
{
  surfFinishedScreen = SurfaceCreate(surfFinishedScreen);
  surface_set_target(surfFinishedScreen);

  shader_set(currentShader);
  shaderColorIndexMod = shader_get_uniform(currentShader, "colorIndexMod");
  shader_set_uniform_i(shaderColorIndexMod, colorIndexMod);

  draw_surface(application_surface, 0, 0);

  shader_reset();
  surface_reset_target();
}

function DrawScreen_DrawLighting()//==============================================================================================================================================================
{

  surfLighting = SurfaceCreate(surfLighting);
  if (surfLightingAlphaVariance < -.9)
  {
    surfLightingAlphaVariance = -.9;
  }

  draw_set_alpha(surfLightingAlpha + surfLightingAlphaVariance);
  surface_set_target(surfFinishedScreen);
  draw_surface(surfLighting, 0, 0);
  surface_reset_target();
  draw_set_alpha(1);

  surfLightingAlphaVariance = 0; //this is added to by flames and stuff to create flickers/more light.
  //reset it each frame, since every flame in the scene will add to this every frame.
}

function DrawScreen_PostProcessing2() //=========================================================================
{
    if(effectShader2 != shader_Passthrough)
    {
        surfEffects = SurfaceCreate(surfEffects);

        switch(effectShader2)
        {
            case shader_Waves:
                draw_set_alpha(1);
                surface_set_target(surfEffects);
                shader_set(effectShader2);
                draw_surface(surfFinishedScreen,0,0);
                shader_reset();
                surface_reset_target();
            
                surface_set_target(surfFinishedScreen);
                draw_surface(surfEffects,0,0);
                surface_reset_target();
                break;
        
            case shader_Pixelate:
                draw_set_alpha(1);
                surface_set_target(surfEffects);
                shader_set(effectShader2);
                u_cellSize = shader_get_uniform(shader_Pixelate,"cellSize");
                shader_set_uniform_f(u_cellSize, pixelCellSize);
                draw_surface(surfFinishedScreen,0,0);
                shader_reset();
                surface_reset_target();
            
                surface_set_target(surfFinishedScreen);
                draw_surface(surfEffects,0,0);
                surface_reset_target();
                break;
        }
    }
}
I'm not sure if I'm missing something silly here, or if there's something weird I don't know about when you ping pong transparent surfaces in GM or something, so I figured I'd ask for some fresh eyes! Thanks as always, everyone! =D

Edit: Okay, I'm doing something stupid with my ping ponging during the PostProcessing() function - right now I'm ping ponging effectSurf2 back onto surfFinishedScreen and then drawing surfFinishedScreen in Draw GUI, and that isn't working. If I skip the ping ponging, and just draw effectSurf2 in Draw GUI, the image displays correctly! I'm sure whatever I'm messing up is really silly now! 😅

tldr: after debugging, it seems like the problem is probably coming from the highlighted code here, but I don't know what's wrong with it:


Edit 2: Problem solved! Recreating surfFinishedScreen before drawing surfEffects onto it fixed the ghosting issue:


I'm still not really sure why, though. I figured it had something to do with drawing onto a "dirty" surface, so I intuitively gave recreating surfFinishedScreen a shot, but I don't understand why that fixed the issue, since I was drawing an opaque (I thought) surface on top of it anyway. If someone could explain why this worked, I'd really appreciate it! Thanks guys!
 
Last edited:

Samuel Venable

Time Killer
I dont mess with surfaces enough to know for sure and that's why I didnt really pay much attention to your code but chances are it's a bug with GameMaker, if you have specified specific coordinates for it to draw on and its drawing elsewhere, generally that shouldnt happen
 

FoxyOfJungle

Kazan Games
I noticed that you are not cleaning noise from surfaces, try using draw_clear_alpha(c_black, 0); after surface_set_target();

surface_create():

This function is used to create a surface and will return the index of the surface which should be stored in a variable for future function calls. When the surface is first created, it may contain "noise" as basically it is just an area of memory that is put aside for the purpose (and that memory may still contain information), so you may want to clear the surface before use with a function like draw_clear_alpha().

Test these functions to see what happens:
GML:
gpu_set_colorwriteenable(true,true,true,false);
gpu_set_blendenable(false);

// Code

gpu_set_colorwriteenable(true,true,true,true);
gpu_set_blendenable(true);
I'm guessing, you can test them individually, they deal with alpha etc.
 
@FoxyOfJungle: Heck yeah, thanks! =D
draw_clear_alpha() did the trick. I had no idea surfaces could be "dirty" and need cleaning. The problem is gone when I use this function after setting the target. I'm marking this thread as solved, but I'll still take a technical explanation if anyone wants to give one - I thought that since I was copying surfEffect onto surfFinishedScreen, I wouldn't have any problems, since surfFinishedScreen would (I thought) be completely overwritten with opaque pixels from surfEffect. It's weird to me that that apparently isn't always the case!
 

GMWolf

aka fel666
not immediatelyI'm taking a break from work so i can write this.
Disclaimer: i didn't read any of it in detail.


First of all, i think your main problem is the general pipeline flow: its unecesarily complex with extra surfaces etc.
I understand surfaces are a fairly common way to achieve things in GM, and for a long time my brain worked that way too, but a lot of the time its unnecessary.

Lets break down your effects:
1. Lighting: accumulating light onto a surface: This is pretty straign forwards: you need a surface to accumulate each light. the only inputs are the objects you render.
2. shading: painting the light onto your objects, this is a per pixel effect, meaning each pixel is only affected by that one pixel, it doesnt look at neighboring pixels.
Per pixel effects can be done when redering the object directly, no need to first put it onto a surface. The inputs are the objects you render, and the light surface.
3. postProcess: This is not per pixel, as each pixel needs to look at any other pixel: the input needs to be a surface.


So lets first look at lighting (1). your current code looks perfectly reasonable. It draws all the lights onto a surface and accumulates the result. You have a surface that represents how much light there is at each point.
All objects are going to be affected by that layer surface. So this surface needs to be calculated before you render the rest of the scene (put it on a layer beneath everything else).

Shading (2). Currently you draw everything onto a surface, then light that surface. Why not light the objects in the first place? You can do it with a shader: the shader gets the light from the lighting surface by looking at the fragCoord of the fragment you are drawing.
I recommend doing this using the layer_shader or layer_begin/end functions. apply it to any layer you want to get lit by the lighting surface. Notice you dont need to copy any surfaces or anything.

Post processing(3): This needs to be affect the results of (2). So make sure (2) renders onto a surface rather than the application surface, again, layer_begin/end are your friends. Then you can render that surface using your post processing shader onto the application surface.

Putting it together, your pipeline could look like this:

Code:
// Draw lights to surface
surface: light_surface
   draw_all_lights()

//Draw objects with lighitng applied
surface: scene_surface
shader: shading_shader // this reads from light_surface to apply lighting
   draw_all_object and backgrounds

//Draw pos process
surface: application surface (reset)
shader: post process
   draw scene_surface

in code, using layers, it could look like this:

GML:
function light_layer_begin() {
    surface_set_target(light_surface);
    //do other things like set alpha etc
}

function light_layer_end() {
   surface_reset_target();
}


function scene_layer_begin() {
   surface_set_target(scene_surface);
   shader_set(shd_shading);
   var light_tex = surface_get_texture( light_surface );
   var light_sampler = shader_get_sampler_index(shd_shading, "light_tex");
   texture_set_stage(light_sampler, light_tex);
}

function scene_layer_end() {
   surface_reset_target();
   shader_reset();
}


function scene_layer_post() {
//draw the scene with post processing
   shader_set(shd_post);
    draw_surface(scene_surface);
   shader_reset();
}

layer_script_begin("LIGHTS", light_layer_begin());
layer_script_end("LIGHTS", light_layer_end());

layer_script_begin("SCENE TILES", scene_layer_begin());
layer_script_end("SCENE TILES", scene_layer_end());
layer_script_begin("SCENE OBJECTS", scene_layer_begin());
layer_script_end("SCENE OBJECTS", scene_layer_end());
layer_script_begin("SCENE BACKGROUND", scene_layer_begin());
layer_script_end("SCENE BACKGROUND", scene_layer_end());

layer_script_end("SCENE POST", scene_layer_post());

then in the room, have the following layers (in order):

LIGHTS: add all your lights.
SCENE <...>: all your scene elements that need to get lit, and post processed.
SCENE POST: an empty layer that here just to apply post effects.

any other layer will get rendered as normal: so you can just add some layers with UI elements above the SCENE POST layer and it wont be affected.


What i like about this is how data driven it is, you don't have to hard code how things are rendered, what is lit, etc. its all controlled by the surfaces.

This is essentially a very much improved version of this tutorial i made a while back.
The main improvement is having fewer surfaces and applying the lighting directly.


The main advantages of this pipeline are:
Layer driven: easy to design what gets lit and what doesn't just by placing them in different layers. No need for DrawAllLights(), DrawAllObjects() etc. GM already handles that for you with layers.
Extensible: Easy to define layers that get post processed, but not lit (make a layer that gets the scene_surface target applied, but not the shader.
Faster: you have less copying. you apply your lighting in one go directly when you render your objects.
Smaller memory impact: you only need the two surfaces. and they only need to be scene resolution, not screen resolution. In fact, you can disable the app surface entirely with this and regain some perf. (idk if thats recommended though).
Simpler code: I mean look, what i have above is 90% of the code you need, and it reads well imo.

Disadvantages: maybe more confusing if you are not used to this sort of pipeline?

Is it worth for your project: If you understand the flow, then yeah, i think so.


[Edit] What about the pallet? You can do it in the shading shader, before you apply the light. Or you can do it in the post processing if you want your pallet to affect the final lit result rather than the unlit result.
[Re-edit] why do i share my coding secrets like this?


[re-re-edit] Also yeah, make sure you clear your surfaces. Surfaces will contain garbage when you create them. Also, dont re-create them every frame, thats really expensive, create them once, and re-use them.
 
Last edited:

Nocturne

Friendly Tyrant
Forum Staff
Admin
Moderator
I'm marking this thread as solved, but I'll still take a technical explanation if anyone wants to give one
When you create a new surface you are basically telling GM to reserve a specific chunk of memory to hold the surface data, but you aren't actually writing anything to that memory chunk. So, if the chunk previously contained any data, this will now be what is contained on the surface as "noise", since the memory wasn't cleared before drawing the surface or drawing to the surface. Using the draw_clear_ functions resolves this as it writes over the memory the surface uses with a single set of values, essentially wiping that memory chunk.

:)
 

Juju

Member
You may also want to look at gpu_set_blendmode_ext(bm_one, bm_zero) as this is cheaper than clearing the target then drawing another surface to the target, provided that the target is no bigger than the surface being drawn it to.
 
@GMWolf: Whoa, damn man! Thank you for the book, hahah! I'm not using any layers right now (besides the ones GM auto-creates for me under the hood, I mean), but I'll take a closer look at what you're doing up there. I trust it's some good stuff, and might save me some overhead. My surfaces actually *are* only scene resolution right now, though, I think? I scale the final surface up in Draw GUI after everything else is done in Draw End. =D

When you create a new surface you are basically telling GM to reserve a specific chunk of memory to hold the surface data, but you aren't actually writing anything to that memory chunk. So, if the chunk previously contained any data, this will now be what is contained on the surface as "noise", since the memory wasn't cleared before drawing the surface or drawing to the surface. Using the draw_clear_ functions resolves this as it writes over the memory the surface uses with a single set of values, essentially wiping that memory chunk.

:)
This makes sense to me, but I thought since I was drawing another opaque surface onto the "dirty" surface, that the old noise would already be completely covered by the new data being written on top of it. Clearing the noise first with draw_clear_ solves the problem though, so there must be a bit more too it that I'm not understanding, right? Surfaces with noise on them are unfit for drawing on before being cleared, even if what's being drawn to them is opaque?

Actually, this advice from Juju:
You may also want to look at gpu_set_blendmode_ext(bm_one, bm_zero) as this is cheaper than clearing the target then drawing another surface to the target, provided that the target is no bigger than the surface being drawn it to.
.....seems to be what I'd need to do to correctly draw over a dirty surface, like I was trying to do to begin with, right? =D
I just tried that out, and it works. Just reset the gpu_blendmode back to bm_normal after drawing the surface, right?


Everything's working together now. Thanks, everyone! :D
 
Last edited:

GMWolf

aka fel666
This makes sense to me, but I thought since I was drawing another opaque surface onto the "dirty" surface, that the old noise would already be completely covered by the new data being written on top of it. Clearing the noise first with draw_clear_ solves the problem though, so there must be a bit more too it that I'm not understanding, right? Surfaces with noise on them are unfit for drawing on before being cleared, even if what's being drawn to them is opaque?
if you are using alpha this wont work, since the alpha will blend with what was already there before.
if you do draw over the entire surface with full alpha then you shouldn't need to clear it first.
 
if you are using alpha this wont work, since the alpha will blend with what was already there before.
if you do draw over the entire surface with full alpha then you shouldn't need to clear it first.
That's what I thought, and what makes intuitive sense to me, too. But changing the blendmode before drawing surfEffects (holding a post processed copy of surfFinishedScreen) to surfFinishedScreen like Juju suggested (gpu_set_blendmode_ext(bm_one, bm_zero)), or clearing the surface with draw_clear like Nocturne, Foxy, and Xor suggested fixes the problem. Really weird. I AM drawing the shadow surface right before the post processing surface, and the shadow layer is transparent, obviously. Maybe I'm an idiot, and I'm not drawing the next surface at full transparency in my old code somehow. I was just using draw_set_alpha(1). Is that the wrong function when dealing with surfaces?
 
Last edited:

GMWolf

aka fel666
draw_set_alpha one will set the maximum alpha to 1, but any transparency your surface has will still be transparent. its basically a multiplier,
so if you had any dirty pixels they will show anywhere your surface doesn't have full opacity.
gpu_set_blendmode_ext(bm_one, bm_zero) essentially tells GM not to do any blending, and completely ignores what was already on the surface. It pretty much copies the source pixels onto the destination pixels.
Which, if thats all you are drawing to the surface, then that seems like a prety redundant step, copying from one surface to another. why not use the first surface in the first place?
 
Last edited:
draw_set_alpha one will set the maximum alpha to 1, but any transparency your surface has will still be transparent. its basically a multiplier,
so if you had any dirty pixels they will show anywhere your surface doesn't have full opacity.
That makes sense, but if I just draw surfEffect instead of copying it to surfFinishedScreen and then drawing that, I get the fully opaque, correct result. Meaning It should be correct when I copy it onto surfFinishedScreen too, right, since surfEffect is completely opaque? What the heck is going on here?! 😅

Maybe surfEffect just looks opaque when it's drawn over a black background, and I actually got some transparency in there somehow? I'm taking the original no-lighting scene and copying the lighting surface on top of that, which is transparent. Is it possible that instead of just copying the transparent lighting over the opaque layer, it's actually averaging both layer's alpha levels and bringing down the combined surface's opacity level somehow? I figured a surface with an alpha of 1 with a surface with an alpha of .3 added on top of it would still have an alpha of 1, but maybe something weird is going on. Is there a way to save a surface out to a PNG or something so I can check the alpha levels of each surface? This is really bizarre to me.

Which, if thats all you are drawing to the surface, then that seems like a prety redundant step, copying from one surface to another. why not use the first surface in the first place?
I actually tried while debugging, and I could definitely do it, because the results displayed correctly. It's just that I'm building up my game scene by copying different surfaces onto surfFinishedScreen and then drawing that surface in the GUI event. I could save myself a surface copy at the end with some conditionals, you're right. I'm just lazy and prefer simplicity and readability over performance. Sometimes surfFinishedScreen has like three surfaces ping ponged onto it, and sometimes it has one. Instead of trying to figure out which surface to draw last, I just draw surfFinishedScreen no matter what, hahah.
 
Top