How would one make these screen effects?

This is not an actual request, but if someone does knoe how to do it, please do make a demo.
Also please watch this video first: https://www.youtube.com/watch?v=zjQik7uwLIQ

I have made some screen effects, like a water screen effect
skillissuegemtree
and a heat screen effect
skillissuegemtree
But how would one make wavy effects like this?

Or a ghost-like effect like this?


Or a weird squishing effect like this??
(https://thumbs.gfycat.com/UnevenConventionalLeafbird-max-1mb.gif)

Success! Although this is one effect, its still really cool to see:
skillissuegemtree
I plan it to be a extension.
Heres the github link:
https://github.com/gasanchik/pxt-imgfx

1 Like

Now with setColumn and getColumn done, even vertical screen fx like this can be done!
skillissuegemtree

The setColumn and getColumn is pretty laggy on hardware. setRow and getRow isn’t. How come that?
Heres the code:

   export function getColumns(img: Image, y : number, dst: Buffer): void {
        let dp = 0
        let sp = 0
        let w = img.width
        let h = img.height
        if (y >= h || y < 0) {
            return
        }

        dst.setUint8(dp, img.getPixel(0, y))
        let n = Math.min(dst.length, (w - y) * h)
        //uint8_t * dp = dst.data;
        //let n = min(dst.length, (w - x) * h) >> 1;

        while (n--) {
            dst.setUint8(sp, img.getPixel(sp, y))
            sp++;
        }
        return
    }

    export function setColumns(img: Image, y: number, src: Buffer): void {
        let dp = 0
        let sp = 0
        let w = img.width
        let h = img.height
        if (y >= h || y < 0) {
            return
        }

        let n = Math.min(src.length, (w - y) * h)
        //uint8_t * dp = dst.data;
        //let n = min(dst.length, (w - x) * h) >> 1;

        while (n--) {
            img.setPixel(sp, y, src[sp])
            sp += 1
        }
        return
    }

Good question!

There are two reasons why this is slower on hardware:

  1. getRows and setRows is implemented in C++. Our compiled TypeScript code is pretty fast, but C++ is just a bit faster
  2. The images are stored in memory column by column rather than row by row.

To clarify the second point, here’s a more detailed explanation:

You can think of the memory of the device as one long array of numbers. Since it’s a 1d array and images are 2d, we need to “flatten” the image to store it in memory. This is done by storing all of the columns one after another end to end. For a 2x2 image, it looks something like this:

Memory address:     0x0   0x1   0x2   0x3
Pixel:             (0,0) (0,1) (1,0) (1,1)

(this isn’t quite accurate because each pixel is actually half a byte, but it’s good enough for this example)

When we want to access a specific pixel, we’d do so like this:

color = memory[x * height + y]

Because all of the pixels in a given column are stored next to each other, reading/writing an entire column is fast because it’s all in one contiguous piece of memory.

Now why does being a contiguous piece of memory matter?

Well, computers have several layers of memory that they use to store data. You’re probably familiar with two of these: the hard drive and RAM. RAM is way faster than the hard drive, so when the computer needs to access something often it will move the section of memory it lives in into RAM. It’s expensive to put something into RAM, but it ends up being way, way, faster in the long run if it’s being accessed multiple times. This strategy works best when things being accessed are next to each other in memory, because then only one chunk of data needs to be moved into RAM at a time.

When you access a lot of memory addresses far away from each other, the computer needs to keep moving more stuff into RAM every time something is missing. That’s why it’s better to access stuff in the order it’s stored.

In reality, most computers have more than just two layers of memory. Each one gets smaller and faster the closer it is to the top.

2 Likes

I only expected it to be because of the c++ it was written in!
But, do you know how to make a blitColumn function?
I cant seem to convert your c++ code into ts. Its a bit complicated for me since i donw know c++.

Can you give me a link to the code you’re using get/set columns on? I’m wondering if maybe it can be rewritten to use get/set rows.

Code that uses blitrow (could use getrows and setrows but I don’t know how to do that)

Hi @hasanchik @richard

Very interesting topic for me, thought I am not good at it.

For in each time screen effect applying, get/setColumn will be called many times(100~160 I guess).
So just supposed:

  • create a cache image which transposed screen(x<–>y) at begin of apply()
  • then call get/setRow in C++
  • and write back to screen at the end tranposed again

Is that a efficient way?

@AqeeAqee that would certainly speed things up, but the screen is rather large (10kb) and keeping around an extra copy might cause memory issues.

@hasanchik have you tried using image.blit (not blitRow). Not sure how the perf will compare, but it might work for your needs

1 Like

Could you also help me with a new function im creating? It basically loops an image with no seams in a canvas of w width and h height. It looks really janky and bad and i dont know how to make it. Its like the scrolling background extension you made but with support for any size image.

The right and bottom part is cut out, that is what i mean with “looping a image with no seams in a canvas”. Here are the scripts:

namespace Math {
    export function mod(a: number, n: number): number {
        if (n == 0) {
            return a
        }
        return (((a % n) + n) % n);
    }
}
////////////////////////////
    export function repeatImage(img: Image, scrollx: number, scrolly: number, maxwidth: number, maxheight: number, scrollable : boolean = false) {
        let w = img.width
        let h = img.height
        maxwidth = maxwidth + w
        let og = img.clone()
        let out = image.create(maxwidth,maxheight)
        let modx = maxwidth
        let mody = maxheight
        if (scrollable == true) {
            modx = 0
            mody = 0
        }
        for (let y = 0; y < Math.ceil(maxheight / h); y++) {
            let y2 = Math.mod(h * y + scrolly, mody + Math.mod(maxheight, h))
            for (let x = 0; x < Math.ceil(maxwidth / w); x++) {
                let x2 = Math.mod((w * x + scrollx), modx + Math.mod(maxwidth, w))
                out.drawTransparentImage(og, x2, y2,)
            }
        }
        return out
    }

skillissuegemtree

i dont think that would work, because the screen is a rectangle. If you transposed the screen you would lose alot of image data.

@hasanchik you could transpose it to an image with the height/width swapped

@hasanchik give this a shot:

1 Like

Thanks alot! This will be a really good extension thanks to you!

Yes, indeed. I ignored 10kb memery is so critical for embeded devices.
Maybe some simple project could use it for preformence, absolutely bad idea for extensions.
Thanks.

1 Like

@hasanchik That’s what I mean.
Sorry for my English, it’s not my native language.

1 Like

Hi again,
I’ve never mentioned this image effect i made: Its a dithering effect. It can dither from one image to another, from an image to a color, or simply fade. Its like that morph extension. Take a look:
skillissuegemtree
Problem is, this is incredibly laggy!!! On my xtron pro, when i applied this effect to a 16 by 16 sprite, fps drops to 20. Then i applied it to a 32 by 32 sprite, it drops to 10 fps!! And when i apply it to the whole screen like the gif it drops to 0.4 fps!!! Maybe because im setting every pixel, but that cant be it alone. I really dont know how to optimize this.

Code:

    //Thanks to kwx for the basis of this effect! (From 3d rendering demo)
    const Dither = img`
        1 1 1 1 . 1 1 1 . 1 1 1 . 1 1 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . . . 1 . . . 1 . . . 1 . . . .
        1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 . 1 1 1 . 1 1 1 . 1 1 1 . 1 . . . 1 . . . 1 . . . 1 . . . . . . . . . . . . . . . . . . . . .
        1 1 1 1 1 1 1 1 1 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . . . . . . . . . .
        1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 . 1 . 1 . 1 . 1 . 1 . 1 . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . .
    `
    export function dither(img: Image, threshold: number, color: number = 0, img2: Image = null, offx: number = 0, offy: number = 0) {
        let w = img.width
        let h = img.height
        //let imageData : number[] = []
        let og = img.clone()
        for (let y = 0; y < h; y++) {
            const screeny = (h >> 1) + (y | 0) - 1 + Math.abs(offy)
            for (let x = 0; x < w; x++) {
                const ditherOffset = Math.floor(Math.mod(threshold, 17)) * 4;
                let screenx = (w >> 1) + (x | 0) + Math.abs(offx)
                let ditherX = ditherOffset + (screenx % 4);
                let ditherY = screeny % 4;
                let ditherPixel = Dither.getPixel(ditherX, ditherY);
                let shaded = ditherPixel ? 1 : 0;
                if (shaded>=1) {
                    og.setPixel(x, y, color)
                    if (img2 != null) {
                        og.setPixel(x, y, img2.getPixel(x,y))
                    }
                }
            }
        }
        return og
    }
2 Likes

Some quick suggestions:

  • Move as much code as possible out of the inner loop. For example, ditherOffset only depends on threshold, and can be calculated once at the start of the function.

  • Use buffer functions (image.getRows/setRows) instead of accessing individual pixels

  • Try unrolling loops. For example, here’s a function from the Space Rocks 3D renderer that draws an alternating-colors dither pattern in chunks of 8 pixels, then doing the leftover pixels in chunks of 4/2/1 pixels as needed to get the exact pixel count needed. This is a lot faster than a per-pixel loop since it avoids the overhead from jumping around.

    export function drawTriLineAlternating(tri: ActiveTrapezoid, buf: Buffer, col0: number, col1: number) {
      const y0 = Fx.toIntFloor(tri.a_y)
      const y1 = Fx.toIntFloor(tri.b_y)
    
      let y = y0
      if (y & 1) {
          // Swap the colors so that col0 is consistently on even Y
          const tmp = col0
          col0 = col1
          col1 = tmp
      }
    
      let y1end = y1 - 7
      while (y <= y1end) {
          buf.setUint8(y, col0)
          buf.setUint8(y + 1, col1)
          buf.setUint8(y + 2, col0)
          buf.setUint8(y + 3, col1)
          buf.setUint8(y + 4, col0)
          buf.setUint8(y + 5, col1)
          buf.setUint8(y + 6, col0)
          buf.setUint8(y + 7, col1)
          y += 8
      }
      if (y <= y1 - 3) {
          buf.setUint8(y, col0)
          buf.setUint8(y + 1, col1)
          buf.setUint8(y + 2, col0)
          buf.setUint8(y + 3, col1)
          y += 4
      }
      if (y <= y1 - 1) {
          buf.setUint8(y, col0)
          buf.setUint8(y + 1, col1)
          y += 2
      }
      if (y <= y1) {
          buf.setUint8(y, col0)
          //++y
      }
      tri.a_y = Fx.add(tri.a_y, tri.a_dydx)
      tri.b_y = Fx.add(tri.b_y, tri.b_dydx)
    

    }

2 Likes