Skip to content
View in the app

A better way to browse. Learn more.

ResHax

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
Help us keep the site running.
Zero Tolerance for Disrespect

NexusTK EPF Transparency Mask Format - Standard or Custom?

Featured Replies

Hello!

I'm trying to understand a binary format of an image transparency mask for NexusTK - a 2D MMORPG from 1996.

(Disclaimer: I received help back in the XeNTaX days, and no longer have a way to contact who helped, so my understanding of this format is not great - that's why I'm asking here :classic_biggrin:).

To build my transparency masks, I use the following code - it's GDScript so the code is python-ish:

var STENCIL_MASK := 0x80
var mask_image := Image.create(width, height, false, Image.FORMAT_RGBA8)

for i in range(height):
    var total_pixels := 0

    while true:
        var pixel_count := read_u8()

        if pixel_count == 0x0:
            break  # End of mask

        var should_draw := false
        if pixel_count > STENCIL_MASK:
            should_draw = true

        if should_draw:
            # Not sure why this works
            pixel_count = pixel_count ^ STENCIL_MASK

        for j in range(pixel_count):
            var pixel_color := Color.TRANSPARENT
            if should_draw:
                pixel_color = Color.BLACK
            mask_image.set_pixel(total_pixels, i, pixel_color)
            total_pixels += 1

    if total_pixels < width:
        for j in range(width - total_pixels):
            mask_image.set_pixel(total_pixels, i, Color.TRANSPARENT)

An example mask and image are:

mask.png.70df7c024c830cd0918c4b8f13531d80.pngimage.png.98b8153e4e62260f0f9a6ad99355c016.png

The mask is used to blit out the background pixels that aren't suppose to be rendered (pink pixels above).

Is this image mask format common or custom (like I'm not sure why 0x80 is a special value)?

Is there a better way I can build this transparency mask, instead of pixel by pixel?

Thanks!

-DizzyThermal

Solved by piken

  • Solution

> no longer have a way to contact who helped

Greetings again Dizzy.

> Is this image mask format common ...

Yes. RLE byte masks using the high-bit for the opacity and lower 7 bits for the run length count are pretty common (I've seen them a few times before).

> Not sure why this works

Because you're effectively masking off bits to just get the count, like you said x & 0x7F or x - 128.

> I'm not sure why 0x80 is a special value

A game could have arbitrarily chosen to use the low bit (0x01) instead and store the run in the upper bits (needing a >> 1), but using the high bit is easy to test and just requires a mask of 0x7F to get the count.

> Is there a better way I can build this transparency mask, instead of pixel by pixel?

Shrug, I know 0% about GDScript, but I can say that often "set pixel" functions on various image classes can be slower than desired, and that it's often efficient to supply all the data at once from a byte array. I see there's a set_data function, which might be faster, but measure your performance if you care ⏱️.

Edited by piken

  • Author

RLE is indeed what this is!

I just need to figure out how to hand the whole bytearray off to set_data function instead of determining and setting each pixel manually.

I'll follow up with my timing results when I can.

Thanks again piken, and hello again! :classic_biggrin:

  • Author

I tried to build a PackedByteArray and create the image from that, but since I have to still go pixel-by-pixel as I decode the RLE to build it up I'm not sure I'm saving much time.. However, I think it's more readable, so I'll keep this iteration:

var STENCIL_MASK := 0x80

var mask_byte_array := PackedByteArray()
mask_byte_array.resize(width * height * 4)

var byte_offset := 0
for i in range(height):
	var total_pixels := 0
	while true:
		var pixel_count := read_u8()
		if pixel_count == 0x0:
			break

		var should_draw := false
		if pixel_count > STENCIL_MASK:
			should_draw = true

		if should_draw:
			pixel_count = pixel_count ^ STENCIL_MASK

		var pixel_color := Color.BLACK if should_draw else Color.TRANSPARENT
		for j in range(pixel_count):
			mask_byte_array.encode_u32(byte_offset, pixel_color.to_abgr32())
			byte_offset += 4
			total_pixels += 1

	if total_pixels < width:
		for j in range(width - total_pixels):
			mask_byte_array.encode_u32(byte_offset, Color.TRANSPARENT.to_abgr32())
			byte_offset += 4

var mask_image := Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, mask_byte_array)

 

I didn't know this was RLE and that there was an actual format for it, thanks for the knowledge

Edited by DizzyThermal
Add new RLE decoding algorithm

Create an account or sign in to comment

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.