The TRANSPARENCY Extension! šŸ‘ļøā€šŸ—Øļø

rando-muser/arcade-transparency: A MakeCode Arcade extension to render semi-transparent sprites.

Have you ever wanted to make a ā€˜semi-transparent’ or translucent sprite, where you can see the tilemap through them? :eyes:

Thanks to some math (Inspired by the lanterns extension and with many user’s help in this thread) now you can! :partying_face:

Just use the ā€˜Make Transparent’ block to make your sprite see-through, or expand it to specify an ā€˜opacity’-- That’s how opaque the spright looks, from 0 (can’t see the sprite at all, only the background) to 100 (the sprite is fully visible, you can’t see the background at all).

You can also use ā€˜remove transparency’ to undo the effect, ā€˜toggle transparency’ to switch whether a sprite is transparent, and the ā€˜is transparent’ boolean to check if your sprite is currently transparent or not.

Here’s a demo of what the extension can do (A to toggle transparency, hold B to fade in and out, WASD to move, menu to test the isTransparent block):

It would be a good extension to use for:

  • Stealthy, ghost-like characters blending into the background
  • Translucent objects like glass and water
  • Clouds, fog, and colorful smoke to add ambiance to a scene

I would love to hear of any bug reports or more features you want added to the extension, and especially anything you make with it! :slight_smile:

rando-muser/arcade-transparency: A MakeCode Arcade extension to render semi-transparent sprites.

30 Likes

Noice

4 Likes

Ms president you are goated

4 Likes

wow, this effect looks AMAZING! just perused your code a bit and you did an awesome job!

i’ve got some tips/suggestions which you can choose to take or leave

Use the //% whenUsed annotation

whenever you declare a variable inside the top level of a namespace (that is to say, inside a namespace but not inside a function), you should add a //% whenUsed annotation above it. this annotation tells the compiler to only include that variable in the code if it’s actually reference in the user’s program.

for example, you could add it to each of the variables here

Cache colors to speed things up

the work you’re doing here is pretty expensive since it’s being done once per pixel for every transparent sprite on every frame. if you want to speed things up, i’d recommend caching the ideal color for each color combo so that you don’t have to constantly recalculate the lowest distance.

here’s an idea for how to write a function that caches the color:

let colorCache = control.createBuffer(256);

function lookupColor(spriteColorIndex: number, backgroundColorIndex: number) {
    const cacheIndex = (spriteColorIndex * 16) + backgroundColorIndex;

    if (colorCache[cacheIndex] === 0) {
        colorCache[cacheIndex] = calculateLowestDistanceColor(spriteColorIndex, backgroundColorIndex)
    }

    return colorCache[cacheIndex]
}
A handy bitshifting trick

looks like you’re doing a lot of work here to extract the red, green, and blue values of each color in the palette. there’s actually a much easier way to extract those numbers using bitshifting!

note: bitshifting can be a little tough to wrap your head around if you aren’t used to thinking about bytes/numbers in terms of 1’s and 0’s. if you don’t understand the comments in the code below, don’t worry about it! i think this is a concept that is much easier to understand with some diagrams/images that explain things

// integers are 32 bits, meaning they contain 4 bytes of data. the number
// returned by p.color() uses the lower 3 bytes of that data to store the
// red, green, and blue color values for the palette color.
// 
// it looks like this:
// byte 1 (bits 0-7)   is the BLUE channel
// byte 2 (bits 8-15)  is the GREEN channel
// byte 3 (bits 16-23) is the RED channel
// byte 4 (bits 24-31) is unused (all 0s)
// 
// or, if we were to write out the individual bits, it would look like
// this:
// 0000 0000 RRRR RRRR GGGG GGGG BBBB BBBB
// 
// note that we're writing this from highest bit to lowest bit, so all the
// blue bits are on the right of our number!
function unpackColor(color: number) {
    // first, let's extract our blue value, to do that we want to grab the 
    // lowest byte. luckily, there's an easy way to do this using the bitwise & operator!
    // 
    // the & operator takes all the bits in the left and right integers and returns
    // a number where only the bits that are 1 in both inputs are set.
    // 
    // for example: 01010101 & 00001111 = 00000101
    // there are only two bits that are 1 in both numbers, so those are the only
    // ones set in the result!
    // 
    // another way of thinking about the & operator is that you can use the right
    // number to "filter" out bits from the left number. if we want to get the lower
    // 8 bits of our number, we just need to & it with something that has only the lower
    // 8 bits set to 1!
    //
    // in other words:
    // 0000 0000 RRRR RRRR GGGG GGGG BBBB BBBB &
    // 0000 0000 0000 0000 0000 0000 1111 1111 =
    // 0000 0000 0000 0000 0000 0000 BBBB BBBB
    //
    // we can write out that number with only the lowest 8 bits set as 0xff
    const blue = color & 0xff
    
    // the right shift operator ">>" shifts all the bits in a number
    // to the right by the number you specify. all of the bits that
    // get shifted past the lowest bit are chopped off and replaced with
    // 0s on the left side.
    //
    // for example: 0110 1001 >> 4 = 0000 0110
    // we shifted the upper 4 bits (0110) to the right and the lower 4 bits
    // were chopped off!
    //
    // to get our green value, we can move all of our bytes "down" by shifting
    // them to the right by a multiple of 8. in this case, we want to move down 1
    // byte, so we shift to the right by 8.
    //
    // just like with our blue value, we also use & 0xff to chop off everything but
    // the lowest 8 bits
    const green = (color >> 8) & 0xff

    // finally, we can get the red byte by shifting "down" two bytes (aka 16 bits).
    // for this one, we don't need to bother using the & operator since we know that
    // all the bits above the red byte are already 0s
    const red = color >> 16;

    return [red, green, blue]
}
Getting the color underneath a pixel

i like the trick you did here for getting the color underneath a pixel! however, as i’m sure you’ve realized, this won’t work for sprites on top of other sprites. if that’s a scenario you’re interested in supporting, then i’ve got an idea you can use to do it!

my arcade-drawing-utils extension has an API that can be used to add code that gets called right before a sprite is drawn to the screen. using that, you can read the background colors directly off of the screen image!

drawing.renderOnSprite(mySprite, drawing.RenderOrder.Below, () => {
    // don't actually draw anything here, just update the transparent image.
    // this code will run as the screen is being rendered, so instead of your
    // getColor(x, y) function, you can just use screen.getPixel(x, y)
})
12 Likes

Ah! Finally we can add ghosts to our games! And glass… and fancy fog effects maybe?? I’m definitely going to be looking at the code to see if some other effects could be made with a few edits, such as tinting things a certain color (blue for waterfalls?!?)

8 Likes

so like opacity

3 Likes

now you can basicly make differ colors with that! or shades I guess but nice!

1 Like

does this work with part of a sprite? (like a fly)

2 Likes

This is the extension we needed thank you. For this gift of awesomenesstisism

1 Like

awesomeness tism bro what

1 Like

So cool! Now I want to make a cloaking system.

1 Like

Random use u still didn’t add this to your bio

1 Like

Amazing extension! You can also use them as heat waves if you put transparency to zero.

3 Likes

I’m going to use it as a steam effect for my Detective Beetle game!
Really nice, randomuser

4 Likes

I’d love to use this as shadows, but I’m afraid it’ll gut the performance

4 Likes

I may use this in cube labs if its compatible with all of the other extensions im using and if the performance stays good.

2 Likes

Woah, thank you so much, I’m really glad you like it! :blush:

#1: Added, I’m glad that exists to help clear up some storage!
#2: Ooo, I hadn’t thought of that, that is great for optimization! The one snag with it is that the opacity of the sprite changes, which has to be factored into calculating the lowest distance color, meaning each opacity would have to have its own buffer.
However, since 50% opacity should have the best contrast and is the default opacity, I’ve added a cache for that opacity & as a block for users to force the code to create a buffer for a certain opacity. LMK if you think a buffer for each opacity would be worth the storage :thinking:
#3: Woahh that is an elegant solution (And explanation of it!), I added that, it will save so much processing power.
#4: That sounds like exactly what I need here, but I don’t think I know how to implement it. I have this code running (just in the namespace, not within any function or loop) and it gives me the error ā€˜assertion failed’. I think it might have to do with the fact that I need to update which sprites are using the drawing.renderOnSprite code each frame, but that code is designed to be set once. (if I run the drawing.renderOnSprite code every frame, the screen goes black and freezes)


Let me know if you know how to implement that code while being able to update which sprites it should run on!

3 Likes

ah, that message usually means that you hit an error in the makecode compiler; i’ve noticed it’s sometimes touchy when dealing with functions declared in weird places; seems like it doesn’t like the arrow function you’re passing to the renderOnSprite call. try moving that code into a function instead of in the top-level of a namespace

as for the caching, i wasn’t thinking about opacity. it probably still makes sense to have some caching somewhere, though… starting with just the default level is a good idea.

caching for every opacity level would certainly be too much memory, but it would probably good to have a second cache that you clear whenever the opacity changes.

my guess would be that their are two primary scenarios for this extension:

  1. fade in/out effects for making sprites appear and disappear
  2. sprites that have a constant opacity, like smoke/particle effects or a ghost

for the first scenario, the opacity is constantly changing so the cache is going to get cleared a lot. it’ll still be beneficial for big sprites, however, since they will probably have a lot of repeat pixel combinations while rendering a single image. for example, imagine the ice cream cone over a green background; the white on top of green pixel is going to come up many times in a single render. i think you’ll probably still see a good performance bump.

for the second scenario, my guess is that most sprites will share the same opacity so the cache won’t have to be cleared all that often. that’ll be a good performance boost.

2 Likes

HUGE EXTENSION!!! I literally never thought this day would come, so many crazy extensions these days

5 Likes

also, you could cache for each level if you limited the number of opacity levels.

when i was writing my code before, i used a buffer that was 16x16=256 bytes long; that was really just me being lazy, since there are really only 15 colors and you only need half a byte to store each value.

so say we limited the opacity effect to only have X possible values, the calculation of the number of bytes would look like this:

15 * 15 * X / 2 = total size in bytes

for 16 different opacity levels, that would be 1800 bytes or about 1.8 kilobytes, which isn’t that bad. i mean, it’s a lot but it’s still nothing compared to having a screen sized image in memory. you could play around with the number of bands to see what values still allow for a smooth looking fade in/out effect.

implementation wise, you should use an image instead of a buffer. images already handle the half a byte per pixel thing, so you won’t have to do any extra bitshifting to make that happen. just make an image that is 15x15 pixels wide and X pixels tall, then you can treat the x in the image as the index in the cache and y as the opacity level.

2 Likes