Register FAQ Search Today's Posts Mark Forums Read
Go Back   JazzJackrabbit Community Forums » Open Forums » JCS & Scripting

New cutting-edge method of making backgrounds in JJ2+ (16 bit mode only)

DennisKainz DennisKainz's Avatar

JCF Member

Joined: Dec 2005

Posts: 416

DennisKainz is notorious for his worthless posts

Apr 20, 2024, 03:46 AM
DennisKainz is offline
Reply With Quote
New cutting-edge method of making backgrounds in JJ2+ (16 bit mode only)

I just found an edge-cutting method of creating 16-bit color backgrounds in JJ2+!
Essentially, you subdivide an image in its cyan, magenta and yellow values, then you put them in front of eachother, and on sprite mode you choose Blend Multiply.
With this little trick, I managed to insert a 16 bit color image in a level. Here's what it looks like:

The only downside is that it doesn't work in 8 bit mode, but who uses 8 bit mode nowadays?
To prove you the image isn't counterfeit, I linked the level and tileset.
https://www.dropbox.com/scl/fi/7fye0...=uddu5hnk&dl=0
__________________
Free will was a mistake.
- God
Violet CLM Violet CLM's Avatar

JCF Éminence Grise

Joined: Mar 2001

Posts: 11,012

Violet CLM has disabled reputation

Apr 20, 2024, 09:40 AM
Violet CLM is offline
Reply With Quote
This is neat! But I think it's mostly a showcase of how fast tiles are. I tried to adapt it more generally and it seems to be slower than TrueColor, not to mention it relies on a specific palette and I don't think the math works for alpha channel.

Compare a TrueColor version with this addition to Cottage.j2as to draw as sprites instead of tiles:
Code:
uint cottageFrameID;
void onLevelLoad() {
	jjAnimSets[ANIM::CUSTOM[0]].allocate(array={3});
	cottageFrameID = jjAnimations[jjAnimSets[ANIM::CUSTOM[0]]];
	for (int i = 5; i <= 7; ++i) {
		jjLayerHasTiles[i] = false;
		jjPIXELMAP(0,0, 640,480, jjLayers[i]).save(jjAnimFrames[cottageFrameID + i - 5]);
	}
}
void onDrawLayer8(jjPLAYER@, jjCANVAS@ canvas) {
	canvas.drawSpriteFromCurFrame(0,0, cottageFrameID);
	canvas.drawSpriteFromCurFrame(0,0, cottageFrameID + 1, mode: SPRITE::BLEND_MULTIPLY, param:255);
	canvas.drawSpriteFromCurFrame(0,0, cottageFrameID + 2, mode: SPRITE::BLEND_MULTIPLY, param:255);
}
(I did also compare to BLEND_DARKEN and BLEND_BURN, but MULTIPLY seems to be the fastest of those.)
__________________
Seren Seren's Avatar

JCF Member

Joined: Feb 2010

Posts: 866

Seren is a name known to allSeren is a name known to allSeren is a name known to allSeren is a name known to allSeren is a name known to allSeren is a name known to all

Apr 20, 2024, 02:57 PM
Seren is offline
Reply With Quote
Back in late 2015 I wrote a piece of AngelScript that would take an image and output another, plus a palette, ready to be used in a tileset (except the wrong file format because outputting a bmp is awfully inconvenient).

I don't think we had layer sprite modes at the time, or if we did, they were very inefficient. The technique I used instead relied on translucent tiles and as such was 100% compatible with vanilla JJ2. Given that it only required 2 layers, only one of which was translucent, it was also lightning fast and only took up twice the space of a regular 8-bit tileset. There were some 16-bit pixel values it wasn't capable of outputting but you wouldn't really notice it.

The code is available below. Unfortunately I will not provide any assistance in figuring out how it works and there's a nonzero chance my process involved some manual adjustments after running the code (such as rearranging the colors and inserting some new ones to make the sprite palette look mostly right? Who knows!).

Code:
#pragma require "SEhicolor.bmp"
class IndexPair {
	int foreground, background;
}
void onLevelLoad() {
	const array<uint8> redValues = {0, 50, 100, 140, 200, 255};
	const array<uint8> greenValues = {0, 25, 54, 107, 144, 201, 230, 255};
	const array<uint8> blueValues = {0, 95, 105, 185, 255};
	const array<const array<uint8>@> componentValues = {redValues, greenValues, blueValues};
	const array<int> componentWeight = {1, blueValues.length(), blueValues.length() * greenValues.length()};
	const array<int> missingRedValues = {2, 28, 31};
	const array<int> missingGreenValues = {2, 8, 17, 31, 45, 54, 60, 63};
	const array<int> missingBlueValues = {1, 4, 6, 18, 21, 27, 29, 31};
	const array<const array<int>@> missingValues = {missingRedValues, missingGreenValues, missingBlueValues};
	array<array<IndexPair>> pairSets = {array<IndexPair>(32), array<IndexPair>(64), array<IndexPair>(32)};
	for (int i = 0; i < 3; i++) {
		int shift = i == 1 ? 4 : 5;
		array<IndexPair>@ pairs = pairSets[i];
		const array<uint8>@ values = componentValues[i];
		int length = values.length();
		const array<int>@ missing = missingValues[i];
		int missingLength = missing.length();
		for (int j = 0; j < length; j++) {
			int value = values[j] * 3 >> shift;
			for (int k = 0; k < length; k++) {
				IndexPair@ pair = pairs[value + (values[k] >> shift)];
				pair.foreground = j;
				pair.background = k;
			}
		}
		for (int j = 0; j < missingLength; j++) {
			int index = missing[j];
			pairs[index] = pairs[index - 1];
		}
	}
	jjPAL@ pal = jjPalette;
	int redLength = redValues.length();
	for (int entry = 16, i = 0; i < redLength; i++) {
		uint8 red = redValues[i];
		int greenLength = greenValues.length();
		for (int j = 0; j < greenLength; j++) {
			uint8 green = greenValues[j];
			int blueLength = blueValues.length();
			for (int k = 0; k < blueLength; k++) {
				uint8 blue = blueValues[k];
				pal.color[entry++] = jjPALCOLOR(red, green, blue);
			}
		}
	}
	pal.apply();
	array<jjPIXELMAP@> tiles(3970);
	for (int i = 10; i < 3970; i++) {
		@tiles[i] = jjPIXELMAP();
	}
	jjSTREAM bitmap("SEhicolor.bmp");
	uint offset;
	bitmap.discard(10);
	bitmap.pop(offset);
	bitmap.discard(offset - 14);
	for (int id = 1930, x = 0, y = 31, t = 0, i = 0; i < 0x1EF000; i++) {
		IndexPair pair;
		pair.foreground = pair.background = 16;
		for (int j = 0; j < 3; j++) {
			uint8 component;
			bitmap.pop(component);
			const IndexPair@ src = pairSets[2 - j][component >> (j == 1 ? 2 : 3)];
			int weight = componentWeight[j];
			pair.foreground += src.foreground * weight;
			pair.background += src.background * weight;
		}
		tiles[id][x, y] = pair.foreground;
		tiles[id + 1980][x, y] = pair.background;
		if (++x == 32) {
			x = 0;
			id++;
			if (++t == 60) {
				t = 0;
				if (--y == -1) {
					y = 31;
					id -= 120;
				} else {
					id -= 60;
				}
			}
		}
	}
	for (int i = 10; i < 3970; i++) {
		tiles[i].save(i);
	}
	for (int i = 10; i < 1990; i++) {
		jjTileType[i] = 1;
	}
	for (int id = 10, i = 4; i < 6; i++) {
		jjGenerateSettableTileArea(i, 0, 0, jjLayerWidth[i], jjLayerHeight[i]);
		for (int j = 0; j < jjLayerHeight[i]; j++) {
			for (int k = 0; k < jjLayerWidth[i]; k++) {
				jjTileSet(i, k, j, id++);
			}
		}
	}
	jjSTREAM file;
	string header = "P6 320 12704 255\n";
	int headerLength = header.length();
	for (int i = 0; i < headerLength; i++) {
		file.push(header[i]);
	}
	for (int i = 0; i < 12704; i++) {
		for (int j = 0; j < 10; j++) {
			jjPIXELMAP image(j + (i >> 5) * 10);
			for (int k = 0; k < 32; k++) {
				jjPALCOLOR color = jjPalette.color[image[k, i & 31]];
				file.push(color.red);
				file.push(color.green);
				file.push(color.blue);
			}
		}
	}
	file.save("SEhicolor.ppm.asdat");
	file.clear();
	const string head = "JASC-PAL\r\n0100\r\n256\r\n";
	for (uint i = 0; i < head.length(); i++) {
		file.push(head[i]);
	}
	for (uint i = 0; i < 256; i++) {
		jjPALCOLOR color = jjPalette.color[i];
		string text = color.red + ' ' + color.green + ' ' + color.blue + "\r\n";
		for (uint j = 0; j < text.length(); j++) {
			file.push(text[j]);
		}
	}
	file.save("SEhicolor.pal.asdat");
}
__________________

I am an official JJ2+ programmer and this has been an official JJ2+ statement.
Seren Seren's Avatar

JCF Member

Joined: Feb 2010

Posts: 866

Seren is a name known to allSeren is a name known to allSeren is a name known to allSeren is a name known to allSeren is a name known to allSeren is a name known to all

Apr 20, 2024, 03:13 PM
Seren is offline
Reply With Quote
When I developed this, the prospect of 16-bit tilesets was briefly very exciting to me until I considered the limitations - the inconvenience of use, incompatibility with actual tile types and sprite modes, extra space usage, etc. But the real eye-opener for me was this: here's the screenshot from the first post in this thread but reduced to 128 colors:

__________________

I am an official JJ2+ programmer and this has been an official JJ2+ statement.
Slaz Slaz's Avatar

JCF Member

Joined: Aug 2004

Posts: 1,315

Slaz is OFF DA CHARTSlaz is OFF DA CHARTSlaz is OFF DA CHART

Apr 24, 2024, 02:42 PM
Slaz is offline
Reply With Quote
Imo the inconvenience people have with 8-bit color palettes is more about having to deal with color reduction and/or draw with limited colors in mind than with the final quality of the visuals.

People specifically looking to make a JJ2 tileset will know their stuff sooner or later. But it appears that experienced artists of this day and age willing to draw an episode cover image (like those for the community HH episodes) are alienated by the thought of having to deal with limited colors.

Personally, I think the combination of flattening and dithering, if done right, can give charm to the final result so I don't really feel the need for JJ2 to be compatible with 16-bit tilesets beyond these proof-of-concepts.
__________________
Add SlazRabbit on Xbox Live if you want to play some GoW1/2/3/J or Destiny1/2.
Jazz Jackrabbit 2 Forever!!
Civilian Defence Force - Jazz2 Visual Fantasers
Reply

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is On

Forum Jump

All times are GMT -8. The time now is 08:37 PM.