Sprite Masking - Render Sprites Behind Other Objects

Started by kevin, February 13, 2023, 09:57:29 PM

Previous topic - Next topic

kevin

Sprite Masking - Render Sprites Behind Other Objects

   This demo renders a mock up scene made up of a group of randomly places rectangles.  After the rectangle are draw we draw our ship image over the top.  Now since it was draw after them it'll always be visual, given that we're using painters algorithm to draw the screen.  By that i mean we're drawing the backdrop and then drawing the rectangle elements in order from the back to front based upon their depth values.  

   But what if we wanted to draw our images after the backdrop was drawn,  but still have them still appear as if they're part of the backdrop scene, so the images could be occluded by the already drawn backdrop??

   Well, one way would be to 'mask'  the images by the backdrop elements, or in other words we could clip out sprite to the backdrop by drawing those zones over our images pixels prior to rendering it.  If we draw over the image with it's mask colour then render the image out as transparent,  this will given the illusion that a foreground object is moving behind something in the background even though it's drawn after it.    



   




PlayBASIC Code: [Select]
   openscreen 1200,600,32,1

// ----------------------------------------------------------
// The Original SHIP image
// ----------------------------------------------------------
path$ =ProgramDir$()+"Help\Commands\Media\"

Image_Ship = LoadNewImage(Path$+"Ship.bmp",2)
scaleimage Image_Ship,2,2, 0

Image_Ship_Mask = NewFXImage(GetImageWidth(Image_Ship),GetImageHeight(Image_Ship))

Type tRect
x1,y1,x2,y2
depth
EndType

global Rect_Count=50
Dim Rect(Rect_Count) as tRect

MaxDepth=10


// Randomly position our rects on screen
For lp=0 to Rect_Count
Rect(lp)= new tRect

Rect(lp).X1=rnd(getsurfacewidth())
Rect(lp).X2=Rect(lp).X1+rnd(100)
Rect(lp).Y1=rnd(getsurfaceheight())
Rect(lp).Y2=rnd(getsurfaceheight())
Rect(lp).depth = floor(rnd#( MaxDepth))
next



Dim Depth_PAlette( MaxDepth)
for lp =0 to MaxDepth
Depth_PAlette(lp)=rndrgb()
next



// ------------------------------------------------------------------
// ------------------------------------------------------------------
Do // -------------------- MAIN LOOP -----------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------

// Clear The Screen to RGB(0,0,0) which is black
cls rgb(0,0,0)


// ----------------------------------------------------
// draw a rects from lowest DEPTH to highest
// ----------------------------------------------------
lockbuffer
for ThisDEPTH =0 to MaxDepth
ink Depth_PAlette(ThisDEPTH)
For RectLP=0 to Rect_Count
if Rect(RectLP).depth=ThisDEPTH
box Rect(RectLP).X1,Rect(RectLP).Y1,Rect(RectLP).X2,Rect(RectLP).Y2
endif
next
next
unlockbuffer
ink -1

// Read mouse position
mx=mousex()
my=mousey()

//
drawimage Image_Ship,mx,my,true

// Mask Images
For Angle# =0 to 360 step 30
RA#=BaseAngle#+Angle#
X=mx+cos(RA#)*200
Y=my+sin(RA#)*200
DrawMaskedIMage(Image_SHIP,Image_Ship_Mask,X,Y,mousebutton()<>1, ClipDepth)
next

BaseAngle#=wrapangle(BaseAngle#,1)
print ClipDepth
ClipDepth+=mousemovez()
sync

loop Scancode()<>0






Function DrawMaskedIMage(Image,Mask,Xpos,Ypos,Flags, ClipDepth)

OldSurface=getSurface()


// Copy the IMAGE to the MASK as we're going to damage the mask

Width =GetIMageWidth(Image)
Height=GetIMageHeight(Image)
CopyRect IMage,0,0,width,height,Mask,0,0


// Compute the position of the image in screen/world space
ImageX1 = XPOS
ImageX2 = XPOS+Width
ImageY1 = YPOS
ImageY2 = YPOS+Height


// Here we run through and MASK out the sprite pixels
// against the HARD zones of the rectangles in our dummy
// game world


rendertoimage Mask

For RectLP=0 to Rect_Count
if ClipDepth<Rect(RectLP).depth

X1=Rect(RectLP).X1
X2=Rect(RectLP).X2
swapifhigher X1,X2

if X2>IMageX1
if X1<IMageX2
Y1=Rect(RectLP).Y1
Y2=Rect(RectLP).Y2
swapifhigher Y1,Y2

if Y2>IMageY1
if Y1<IMageY2

// The SPRITE and this zone overlap
// So we have to draw this zone over the sprite

// To do this we convert the screen space
// Coordinates into local space of the image
DX=X2-X1
DY=Y2-Y1

X=X1-IMageX1
Y=Y1-IMageY1

Login required to view complete source code



   Video Transcript:


hello and welcome to another play basic
code review
my name is Kevin Picone I'm the
developer of play basic and I wanted to
show an idea about how you can do Sprite
masking in a manual sense let's have a
look at the concept open up Play basic
down here sorry
open I wrote this example earlier I just
wanted to go through the code a little
bit and give you a sense of what this is
actually doing
we'll run the game for well we'll run
the demo
it's not a game
uh you'll notice the ships are being
occluded by the foreground
and I can move the ships forward in
front of or behind those planes now
well when we take a look at the code
you'll notice that the ships are drawn
at the end of the process
so normally when you draw Graphics of
course you draw something and then you
paint
we're using painters algorithm we're
drawing a backdrop and then drawing the
foreground and then the hard stuff in
front of it and the Order of those
things determines the order of what you
can say
so if we draw our ships at the end of
their process there should shouldn't be
possible for them to appear as if
they're going behind things that's what
we're using this masking idea for
it's quite a common idea it's not not a
new thing it's a kind of an old concept
so what does the program do it first
loads gets the pathway to a graphic that
I know the location of which is in the
playmastic help files so I don't have to
supply you with a graphic I can just
give you the code as is and you could
cut and paste that and it should run
provided that that piece of media is in
your help files it should be
it may not be if you've got a really old
version of play biasing if your version
is the current learning Edition or
current retail Edition then you should
be fine
once you've got the location of that we
load this ship
graphic
we scale it up we'll make it twice the
size and I do something interesting here
as I make a secondary FX image although
it's an image that's stored in system
memory the same dimensions as the ship
image oh
sorry that should be
I did change the the names of the
variables earlier and forgot to change
those ones there
just run again see if we haven't broken
anything that's fine it's okay
right so this mask image was just a
blank image nothing on it and it's the
same size of our image ship
that completes the lighting of the media
process then we set up a very simple
structure to hold our rectangles
which in this case they they simulate a
background of some type
understanding a global variable of 50 so
we have 50 rectangles in this scene
we set up an array so each element or
index in the array is going to have this
structure in it
we run through and randomly create this
backdrop rectangles
so I've got a random position on screen
across the x-axis
and the width
or it's it's right hand component of the
rectangles it's just created by going
give me
the left hand X plus a random value of
up to a hundred
for the Y in positions we don't get like
here we'd skip a couple
the theory is there is I wanted to have
the
if you just go completely crazy with
randoms you end up with with rectangles
that fill the entire screen and
it's not very useful for the
demonstration because you know pretty
much they'll just cover everything
and we're also giving each rectangle a
depth
now I'm drawing these things uh using
painters so I'm
to draw the scene we're going through
and drawing
all the wrecks with the lower step first
then
so any depths of zero then any depth of
one and the in depths of two and so on
so it's not a very efficient way of
doing this this particular thing but
it's just there for demonstration
purposes
all right so we've created our
rectangles we're going to create a list
of colors that apply to each depth and
rectangle so any rectangle that's on a
depth of five will have whatever is in
the depth palette which will be a random
RGB color
okay just for demonstration purposes
the main Loop
we've got our main do Loop structure
so which means at the end of that
we've got a loop
so PBA will see to do the starting side
of the Dual block and
we'll keep rolling down here until it
hits the loop and then
this one here has an expression after it
so it's acting as if it's
a repeat until statement really
because you have Expressions after these
optionally
so there's a loop until a scan key or
any key in the keyboard is press and
that will just eject out of that and run
down to the bottom of the program in
this case there's nothing at the end so
all complete
conclude inside the loop is what's
keeping the game or the demo running
we've got a
first we're clearing the screen to Black
which is
rgb000 that's color black you might also
see this in programs
written like this
CLS 0.
because RGB
equates to an integer value of zero so
some people will shortcut that or you
might yeah looks like we want it white
and we we could use the hex value there
that would give us a value of white
or we could do this to get white
they're functionally the same and so is
this by the way
negative one and understand why I needed
one is the equivalent you'd have to
really dig into how integer numbers work
and sine integer numbers work but
this will give you a hex value of
actually
in HEX maybe one is actually this
and since it's only use the bottom six
digits there which is going to be each
pair is going to be in uh the r which is
going to be this section here
that's the r that's the G that's the B
that would be Alpha Channel if it's if
we had we don't
all right so we're clearing this
clearing our screen to Black and we're
running through and we're drawing all
the rectangles up front
so we're starting from a depth of zero
we're running through and looking for
anything of that particular depth
we're going through all the rectangles
and if it this rectangle happens to be
at the same depth we draw it in the
current ink color for this batch of
rectangles
and we keep doing that until we're done
so we've drawn all the rectangles now
we're going to draw the Sprites
now I just grabbed the mouse position
here
braid Mass position
gives us the X and the y coordinate the
mouse positioning or I'm drawing an
image
using our original image shipping image
which is the silver ship
at this location where the mouse is uh
it's it's going to position the ship
um when we draw it we're drawing from
the top left hand corner of the image
transparently as well
so any um any area of that image that
that is color zero or rgb000
will be screened out of the image and
any other color will be passed through
and drawn
that's the main ship but the orbiting
ships are not drawn like that
mask
images yeah whatever
so I'm just
running through a 360 degree set of
angles at 30 degrees grabbing an angle
here and Computing a
a position around the mouse position
that's 200 pixels away
from this location so if you're filling
with so if you're familiar with polar
coordinates
we've got an angle and a length that's
all it would be really neat and we've
got a the base position is where the
mouse position is so we're Computing
this offset in any uh in any direction
from that point
it's very useful trigonometry is really
useful for a two-day game development in
particular so
well worth having a refresher of that
we call our drawer Mast image
and we're passing in a few different
things we're passing it in the original
ship
our mask image which is this secondary
image that it's the same size as the
original ship you hinge
velocity extra information we need to
draw this thing when we're done with it
we need its coordinates on screen and we
want it to be drawn transparent at the
end of the process
but I also want to know what level we
should clip this uh Sprite again so this
image against these rectangles at the
top there so
what I mean by that is that uh when the
when the Sprite has a depth of zero it
will be
it will
be drawn in front of the back layer of
rectangles but behind all the other
layers of rectangles
they're in front of that if it was drawn
like Lion 20 it would be drawn there
would be 20 layers of rectangles behind
it and then any other layer that was in
front of that it would be clipped
against
how'd that make sense so let's look at
the function this is the only part of
the card that's actually showing the
concept right
so we
when we call this function to draw a
masked version of the image we're
grabbing the original surface the
current drawing surface which in this
case will be the screen generally but
it's good practice again that you get
used to
asking climatic or the current surface
is because I know I'm going to modify
this inside my loop I don't have a
situation where if I call my function it
changes the current drawing surface to
some place unrelated when I return I
want to make sure I restore
any changes I make inside the function I
restore them at the end of the function
so to for the user's perspective it just
feels like a built-in command
so we grab the current surface
we look at the image that was passed in
which is going to be the original ship
image in this case grab its width and
height
then we copy the pixels from this
Source image to our temporary mask
so we gotta damage this image we can
actually right right transparent data
over the top of the image so we need to
preserve the original image and copy
this other one some some other place so
I can we can mutilate it any way we like
and we have to worry about
anything else about damaging the
original
so this is this makes a copy of that
Source image gets copied across onto the
mask
copying the top left hand coordinates
and its width across to
onto the mask at the top left hand
coordinates
next we compute in screen space
where our image is going to would be on
screen
because the rectangle coordinates we're
going to be clipping again it's they're
all in screen space so when we run this
again
it's coordinates for each rectangle here
let's say
uh where is in this screen space in here
whereas our images
we need to know where that Mouse corner
is or it's drawing the Sprite
and then work out
the the little rectangle where the
Sprite is going to be overlapping
if you get what I mean
once we've got the global position
now we need to do the masking so we're
switching rendering across to our mask
version of the image which is the one
we're happy to manipulate and destroy
the pixels on
we're we're going to Loop through all of
the rectangles we we have we're going to
check our clipping depth against the the
depth of each rectangle
so anything that's actually
uh
that has a higher depth in other words
it is a layer closer to us we want to
apply the clipping to now we may or may
not be impacted by it with we may be the
rectangle might be off to the left of us
or it might be off to the right or above
that's all we need to know it's it's our
position in in screen space so we can
clip the two things to each other
actually I'm not clipping them I'm just
drawing them over each other
if we're draw if our current depth is
lower than the rectangle we're looking
at at the moment we go okay
we might have to mask this this
rectangle against our
our ship
to destroy those pixels so it makes so
it makes it appear like it's behind it
so I grab its coordinates it's left and
right we ensure that there they are left
to right so X1 is going to be
the one on the left of the screen and X2
would be on the right so if you had a
rectangle like like there for example X1
would be this side and X2 would be over
on this side here
we do a comparison against
just this on the x-axis alone we just
compare that against the axis up here
that we computed earlier where the image
would be if it was in screen space
if I get an overlap
we fall through and do the same thing
for the y-axis so we're checking to see
if our Sprite is in the same
uh
same shared area on the x-axis and then
we're doing the same thing against the
y-axis if both of those tests meet that
the two rectangles overlap
so grab the top of top of the rectangle
the bottom just make sure they're
they're in the order we expect
do the same comparisons against our
source image in screen space
once we get here we know the two areas
are impacting each other they're over
each other
and now we have to work out how much of
these the mass the Sprite mask that we
need to draw the rectangle over and the
easiest way to do that is to is convert
the rectangle uh
from screen space coordinates into
localizing its coordinates to the image
and all we did all we need to do is
compute this the width and height of the
rectangle which is what that's doing
and then we subtract the rectangles top
left hand coordinate and
we take away the images screen space at
top within coordinate and that gives us
the offset
um
then we control this rectangle
onto that surface and all effectively
clear
because it's been drawing RGB color zero
and the image we know has a transparent
color of RGB color zero
if I draw that over the top it's like
making those pixels transparent
that's the whole process
now you might think well what's a yeah
it's a pretty
man Haley cause solution to try and do
this but
what it does mean is you could you could
have a pre-drawn scene
very elaborate scene really it wouldn't
matter and then have a list of these
clipping planes that you apply
in post so if the background picture is
drawn
as a painting or a picture and you need
something to move behind something else
like you would see in
uh
uh well those games like Monkey Island
and those kind of point-click games
where it's where the ass is out there
drawing a nice a nice image and then you
might have to make the character walk
through the scene as if it was inside it
even though it's drawn afterwards
so you would do that by having masks
here I'm using rectangles but you could
use you know a
any kind of shape actually polygons
other other images that happen to be in
black and use the same principle it just
draws pixels as black pixels over the
top of
to nullify or to to be drawn as mask
color onto the target
so when they're drawn at the very end
they just appear as transparent pixels
okay so let's recap again we're going
through all the rectangles in that in
the same
we're checking if there's a overlap on
the x-axis do they share a region on the
x-axis
if they do we move to the y-axis
do they still overlap if they overlap on
the x-axis and they have lap on the Y
then they must be impacting
we localize the coordinates first we
compute the width and height of the
rectangle
I'm going to compute the top left hand
coordinate local to where where the
image is
because we're drawing to the image we're
not drawing to the screen here we're
drawing to the image which is going to
be now we're drawing an image pixel
space so pixel zero zero
is actually going to be when it's
rendered out to the screen it's going to
be on screen at wherever this image X1
is
but we need to subtract the two from
each other to get the offset
relative to the image
I bet hope it makes sense
once we have it we can draw the
we can nullify the pencils from the from
that image and then we write
we're still rendering back to the old
surface and we draw the Sprite draw the
image at its location but this time we
don't draw the past in original image we
draw the mask image
and just to demonstrate this actually is
doing doing that if I put a um
just a request on Mouse button there
if this equals one right so it would be
transparent when Mouse button is one
and solid when they're not
so
when we draw the images as they actually
as they really are we can say that the
areas around the pixels that are
occluded by rectangles are being
nullified they're being they're being
rendered over
if I push the mouse button and draw them
as transparent surprise now it appears
as if they're going behind
but they're not really
I think if you're going to implement
make a more robust version of this
solution
uh you would still
your Sprite each would have their own
separate mask
rather than share the mask throughout
the whole set
that way when you're doing animation you
could copy from your
Bank of Animation frames into the mask
and then apply any occlusion that's
necessary and only apply if it's
actually going to be drawn to screen
here we're not even checking that like
if I move the Sprites off screen
we're still applying the same masking
process
what we should do is we should check is
does this Sprite even need to be drawn
by not this is just a demonstration
hold the mess button down to apply our
closure the Sprite in the middle won't
because it's drawn just with normal draw
image doesn't uh it'll always appear in
front because it was drawn last
even though the ones around it they're
actually drawn after that sprite's being
drawn
the one in the middle I'm talking about
all right
that was helpful somewhere hopefully it
gives you some some ideas about so how
you might tackle doing you know
completely different types of games that
you might think would be too challenging
to make
they're probably not
uh enjoy that thanks for watching and
I'll see you next time bye
thanks for watching and if you want more
programming content please subscribe to
playbasic.com