How to show silhouette when behind objects

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
Ross
Citizen
Posts: 98
Joined: Tue Mar 13, 2018 12:12 pm
Contact:

How to show silhouette when behind objects

Post by Ross »

I'm working on a top-down 2D game and I would like to show the silhouette of my character and other dynamic objects when they are "behind" walls, trees, etc. Has anyone here done this, can you give me some pointers?

This is the effect I want:
Image

- I'm dynamically sorting the draw order of my objects by their bottom Y-position. Like so:
Image

- Ideally I would like to do this for multiple objects in the scene, at different "depth" levels.

I've been using LÖVE and Lua for a while and done a little bit of shader stuff, but nothing with the depth buffer, stencil buffer, or anything like that.
All the tutorials for this effect that I've found so far have been pretty engine-specific, incomplete, or don't cover my use-case where the draw-order changes all the time.
User avatar
dusoft
Party member
Posts: 510
Joined: Fri Nov 08, 2013 12:07 am
Location: Europe usually
Contact:

Re: How to show silhouette when behind objects

Post by dusoft »

Do you know what's the next Y layer? If so, can you calculate offset of Y and just render (hidden) part of the player shape in gray in this layer?

Also see:
https://love2d.org/wiki/Contact
User avatar
BrotSagtMist
Party member
Posts: 615
Joined: Fri Aug 06, 2021 10:30 pm

Re: How to show silhouette when behind objects

Post by BrotSagtMist »

Render the toplayer to a transparent canvas first.
Next render the player/enemy/shadow/silloute again using a blendmode that does not interact with the transparent pixels.
obey
Ross
Citizen
Posts: 98
Joined: Tue Mar 13, 2018 12:12 pm
Contact:

Re: How to show silhouette when behind objects

Post by Ross »

dusoft wrote: Sun Feb 13, 2022 9:19 pm Do you know what's the next Y layer? If so, can you calculate offset of Y and just render (hidden) part of the player shape in gray in this layer?

Also see:
https://love2d.org/wiki/Contact
I do. All of the objects are in one sorted list. Of course I don't know which ones are overlapping without checking bounding boxes or something. The trouble is how to only render the hidden part of the player, especially since there can be multiple other objects overlapping it.

BrotSagtMist wrote: Sun Feb 13, 2022 9:36 pm Render the toplayer to a transparent canvas first.
Next render the player/enemy/shadow/silloute again using a blendmode that does not interact with the transparent pixels.
Hmm, yeah I'll try that, thanks. Unfortunately that only lets me have one object (or at least, one depth level) that I can show the silhouette for, but it's a start.
User avatar
0x72
Citizen
Posts: 55
Joined: Thu Jun 18, 2015 9:02 am

Re: How to show silhouette when behind objects

Post by 0x72 »

Hey, hope this helps (most likely not the only way):

1. draw normally
2. to off-sceen canvas: draw mask so we know where the silhouette could should be drawn (on objects, not on player, if you have layers - e.g. floor, you might skip drawing it
3. to another off-screen canvas: draw all silhouettes for the player (and other things)
4. combine the two and draw to screen (or wherever)

Code: Select all

local box = function(self, w, h)
  love.graphics.rectangle("fill", self.x - w/2, self.y - h, w, h)
end

local things = {
  -- character
  {
    isSilhouetteable = true,
    x = 200,
    y = 300,
    draw = function(self)
      love.graphics.setColor(1, 0, 0)
      love.graphics.circle("fill", self.x, self.y - 50, 50)
      love.graphics.setColor(1, 1, 1)
    end
  },
  -- obstacles
  { x = 250, y = 250, draw = function(self) box(self, 100 ,100) end },
  { x = 150, y = 380, draw = function(self) box(self, 100 ,100) end },
}
local player = things[1]

local maskC = love.graphics.newCanvas(400, 400)
local silhouetteC = love.graphics.newCanvas(400, 400)

local maskSh = love.graphics.newShader([[
extern bool on = false;
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
  return on ? vec4(0,0,0,1) : vec4(1,1,1,1);
}
]])

local silhouetteSh = love.graphics.newShader([[
extern vec4 tint = vec4(0, 1, 0, 0.5);

vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
  return tint;
}
]])

local combineSh = love.graphics.newShader([[
extern Image mask;
extern vec4 tint = vec4(0, 1, 0, 0.5);

vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
  vec4 m = Texel(mask, texture_coords);
  vec4 s = Texel(tex, texture_coords);

  s.a *= m.r;
  return s;
}
]])

function love.update()
  if love.keyboard.isDown("w", "up") then player.y = player.y - 1 end
  if love.keyboard.isDown("s", "down") then player.y = player.y + 1 end
  if love.keyboard.isDown("a", "left") then player.x = player.x - 1 end
  if love.keyboard.isDown("d", "right") then player.x = player.x + 1 end
end


function love.draw()
  love.graphics.clear()

  -- sort by Y
  table.sort(things, function(a, b) return a.y < b.y end)

  -- draw normal
  for i, v in ipairs(things) do v:draw() end

  -- draw mask (where one can put shiluette)
  love.graphics.setCanvas(maskC)
  love.graphics.clear(0,0,0,1)
  love.graphics.setShader(maskSh)
  for i, v in ipairs(things) do
    maskSh:send("on", not not v.isSilhouetteable)
    v:draw()
  end
  love.graphics.setColor(1, 1, 1)

  -- draw silhouettes
  love.graphics.setCanvas(silhouetteC)
  love.graphics.setShader(silhouetteSh)
  love.graphics.clear(1,1,1,0)
  for i, v in ipairs(things) do
    if v.isSilhouetteable then
      v:draw()
    end
  end

  -- combine both
  love.graphics.setCanvas()
  love.graphics.setShader(combineSh)
  combineSh:send("mask", maskC)
  love.graphics.draw(silhouetteC)

  -- draw to screen
  love.graphics.setShader()
  love.graphics.setCanvas()
  love.graphics.draw(maskC,500, 0, 0, 0.5)
  love.graphics.draw(silhouetteC,500, 300, 0, 0.5)
end

I guess you could be smarter with blendModes or something, but at least it works
Last edited by 0x72 on Mon Feb 14, 2022 10:57 pm, edited 1 time in total.
Ross
Citizen
Posts: 98
Joined: Tue Mar 13, 2018 12:12 pm
Contact:

Re: How to show silhouette when behind objects

Post by Ross »

Ooh, nice. Thank you so much for writing a demo! That works exactly the way I wanted.

I found that I could eliminate the second canvas and just draw each silhouetteable object with a variation of your combine-shader (just to save a bunch of overdraw) if I pass in the size of the mask to convert screen_coords to mask UVs (assuming it's drawn at 0,0).

Code: Select all

local onScreenCombineShader = love.graphics.newShader([[
extern Image mask;
extern vec2 maskSize;
extern vec4 tint = vec4(0, 1, 0, 0.5);

vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
	vec4 m = Texel(mask, screen_coords/maskSize);
	vec4 s = Texel(tex, texture_coords);

	vec4 c = vec4(tint.rgb, tint.a * s.a * m.r);
	return c;
}
]])
...But anyway. Thanks! This is great.

Image
User avatar
dusoft
Party member
Posts: 510
Joined: Fri Nov 08, 2013 12:07 am
Location: Europe usually
Contact:

Re: How to show silhouette when behind objects

Post by dusoft »

BrotSagtMist wrote: Sun Feb 13, 2022 9:36 pm Render the toplayer to a transparent canvas first.
Next render the player/enemy/shadow/silloute again using a blendmode that does not interact with the transparent pixels.
Great advice without any math needed.
RNavega
Party member
Posts: 252
Joined: Sun Aug 16, 2020 1:28 pm

Re: How to show silhouette when behind objects

Post by RNavega »

This has hardware support if you use stencil masking, so a shader wouldn't be needed.
You have to activate the stencil feature on the config file and use some functions to mark the pixels on the stencil buffer to use as mask.

Check the example at the bottom:
https://love2d.org/wiki/love.graphics.stencil
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Semrush [Bot] and 68 guests