How to interpret luminance/saturation values from a lightmap

Started by GarageGothic, Mon 28/04/2008 05:07:15

Previous topic - Next topic

subspark

Quotesince you'd rarely need to change saturation at different spots in the room
Yeah I would also find it useful for turning a color scene into a grayscale scene reminiscent of the 1950's.

The lightmap, shadow, and scale map is something I requested once as an AGS feature long before somebody chose to take it on as a plug-in. This all sounds too good to be true and if I was going to suggest anything it would be to combine your shadow, scale/offset, and lighting plug-in once they are all complete.

How exactly did you get those shadows to work as I was playing with the idea in my head and figured that you could warp or rotate the already flat/horizontal/diagonal (floor) shadow vertically for the walls based on a color code map. How exactly have you approached it? It looks amazing but complex.

The thread for my Z-Depth scale idea seems to have been erased due to server change or something. This was a long while ago. Back in 2.7x.

The idea is this:
To accompany a room background with predrawn lighting conditions like so:


You have a lightmap which pays attention to walkable areas and matches the lighting of the background:


And in a similar fashion, you have a scale map (Z-Depth) which scales the character based on shade (luminance):


Do you think this is too much for one plug-in alone or would this, combined with your shadow plug-in, serve as the ultimate dream tool for anybody wanting to make super uber rooms for their game?

Cheers,
Paul.

Edit:
QuoteWould anyone else find support for luminance > 100% useful?
I have requested this before actually, CJ. I would find it astronomically useful. Incidentally, my game if full of super bright spotlights that nearly swallow the player and I don't really want to keep creating separate views for this.

GarageGothic

Oh, I could easily add the scale map thing. Not sure how many people will use it unless the editing/debug tools are super easy to use, so that's the most demanding task in implementing it.

As I said to tolworthy, I don't intend to merge the ShadowBox module with the LightMap module because it's currently too buggy. But since I need to rewrite parts of it to DrawingSurface code, I may rethink if there's better ways to do it than the current coordinate based wall setup functions.

subspark

QuoteNot sure how many people will use it unless the editing/debug tools are super easy to use
Well I imagined that people would simply create their z-depth maps in Photoshop. Not 'everything' has to be done inside AGS. Designing these depth maps is best left in the hands of a technical artist or an artistically motivated programmer anyway. As for the debug tools, youve got me there. :)

QuoteI don't intend to merge the ShadowBox module with the LightMap module yet
Fair enough. But after all the bugs are ironed out or after you have found a new method of going about shadows, do you plan to integrate the two features together?

Top work mate. Your nothing short of a genius to me.

Cheers,
Paul.

GarageGothic

Quote from: subspark on Fri 02/05/2008 02:25:52Well I imagined that people would simply create their z-depth maps in Photoshop. Not 'everything' has to be done inside AGS. Designing these depth maps is best left in the hands of a technical artist or an artistically motivated programmer anyway. As for the debug tools, youve got me there. :)

The problem with creating them in Photoshop is that you have little color/size reference unless you keep pasting your character in at different scales. With in-game tools, you could move your character around on-screen, set his size for a specific area and the game would provide you with the correct color for that scale. Once painted, you could see him walk around and make adjustments if something looks wrong. But of course you can use external tools if you wish.

QuoteFair enough. But after all the bugs are ironed out or after you have found a new method of going about shadows, do you plan to integrate the two features together?

I intend to keep them as separate modules, but they will be compatible with eachother so you can use both on the same screen.

subspark

QuoteWith in-game tools, you could move your character around on-screen
Thats a very good point but for the meantime I imagine its not too laborious to create them in Photoshop until an engine based dynamic editor has been created. Looking forward to that one.

QuoteI intend to keep them as separate modules, but they will be compatible with each other so you can use both on the same screen.
Thats marvelous. Thanks for the heads up.

Cheers,
Paul.

lemmy101

We would love to have everything we need to convert from HSV to RGB and back.

i.e. for HSV you have a tint function that passes the hue parameter, but this is handled in AGS at the moment as a scaled RGB value that (I assume) is converted to the hue parameter internally.

For me, the ability to specify the hue itself, or a way to ascertain it from the passed rgb would be mega useful. Then we could convert our end to RGB colour spaces, and do light/colour maps, I assume,  in full RGB.

In one part, for e.g. we want characters to be coloured by a fire, depending on where they are stood, but also the light level of the character, and also simulating the siloette effect you get when an object is in front of a bright lightsource. this is easy to do in RGB, less so in HSV unless we do this conversion.

Since we find RGB the more useful colourspace for such things, we'd love to be able to edit the 360 degree hue, 100% saturation and 100% luminance so we can make this conversion back and forth.

Thanks!

lemmy

PS: GG here are two c# functions we have to convert between HSV (or HSL, same difference) and RGB, if you find them useful (HSV values being between 0 and 1).

Code: ags

                public static Color HSL_to_RGB(HSL hsl) 
		{ 
			int Max, Mid, Min;
			double q;

			Max = Round(hsl.L * 255);
			Min = Round((1.0 - hsl.S)*(hsl.L/1.0)*255);
			q   = (double)(Max - Min)/255;

			if ( hsl.H >= 0 && hsl.H <= (double)1/6 )
			{
				Mid = Round(((hsl.H - 0) * q) * 1530 + Min);
				return Color.FromArgb(Max,Mid,Min);
			}
			else if ( hsl.H <= (double)1/3 )
			{
				Mid = Round(-((hsl.H - (double)1/6) * q) * 1530 + Max);
				return Color.FromArgb(Mid,Max,Min);
			}
			else if ( hsl.H <= 0.5 )
			{
				Mid = Round(((hsl.H - (double)1/3) * q) * 1530 + Min);
				return Color.FromArgb(Min,Max,Mid);
			}
			else if ( hsl.H <= (double)2/3 )
			{
				Mid = Round(-((hsl.H - 0.5) * q) * 1530 + Max);
				return Color.FromArgb(Min,Mid,Max);
			}
			else if ( hsl.H <= (double)5/6 )
			{
				Mid = Round(((hsl.H - (double)2/3) * q) * 1530 + Min);
				return Color.FromArgb(Mid,Min,Max);
			}
			else if ( hsl.H <= 1.0 )
			{
				Mid = Round(-((hsl.H - (double)5/6) * q) * 1530 + Max);
				return Color.FromArgb(Max,Min,Mid);
			}
			else	return Color.FromArgb(0,0,0);
		} 

                public static Color HSL_to_RGB(HSL hsl) 
		{ 
			int Max, Mid, Min;
			double q;

			Max = Round(hsl.L * 255);
			Min = Round((1.0 - hsl.S)*(hsl.L/1.0)*255);
			q   = (double)(Max - Min)/255;

			if ( hsl.H >= 0 && hsl.H <= (double)1/6 )
			{
				Mid = Round(((hsl.H - 0) * q) * 1530 + Min);
				return Color.FromArgb(Max,Mid,Min);
			}
			else if ( hsl.H <= (double)1/3 )
			{
				Mid = Round(-((hsl.H - (double)1/6) * q) * 1530 + Max);
				return Color.FromArgb(Mid,Max,Min);
			}
			else if ( hsl.H <= 0.5 )
			{
				Mid = Round(((hsl.H - (double)1/3) * q) * 1530 + Min);
				return Color.FromArgb(Min,Max,Mid);
			}
			else if ( hsl.H <= (double)2/3 )
			{
				Mid = Round(-((hsl.H - 0.5) * q) * 1530 + Max);
				return Color.FromArgb(Min,Mid,Max);
			}
			else if ( hsl.H <= (double)5/6 )
			{
				Mid = Round(((hsl.H - (double)2/3) * q) * 1530 + Min);
				return Color.FromArgb(Mid,Min,Max);
			}
			else if ( hsl.H <= 1.0 )
			{
				Mid = Round(-((hsl.H - (double)5/6) * q) * 1530 + Max);
				return Color.FromArgb(Max,Min,Mid);
			}
			else	return Color.FromArgb(0,0,0);
		} 


Also nice idea for a plugin, GG, I don't think this is "too much" for a plugin at all, or I'd hope not :D will look forward to seeing it as it's something we're experimenting with in AGX.

GarageGothic

I should clarify that this is a module, not a plugin.

Thanks for the code, lemmy. I already have a bit of script that converts RGB values to the S and V part of HSV and then applies them as the Saturation and Luminance value in the tint. I didn't find any need to do the arithmetic for the hue, since I don't really need that value anywhere. But I'll try to add it in a way that makes sense, and also HSL.

lemmy101

Quote from: GarageGothic on Sat 03/05/2008 18:52:00
I should clarify that this is a module, not a plugin.

Thanks for the code, lemmy. I already have a bit of script that converts RGB values to the S and V part of HSV and then applies them as the Saturation and Luminance value in the tint. I didn't find any need to do the arithmetic for the hue, since I don't really need that value anywhere. But I'll try to add it in a way that makes sense, and also HSL.

NP :)

GarageGothic

DISCLAIMER: This is NOT a finished module, merely a proof-of-concept. It is not suitable for public consumption.

I talked to indie_boy in the AGS Stickam room, and he was having some problems with creating a smooth scaling effect using walkable areas. So I decided to whip up an alpha version of my LightMap module so he could see if the module's scaling-map function would be useful for him.

Basically it allows you to use bitmaps (stored as sprites) to specify character scaling and character tint. The color map tints the character the color of the pixel he's standing on in an RGB image. The scaling map scales him depending on a grayscale bitmap where pure black is 5%, 50% gray is 100% and pure white is 200%.

The map dimensions are in 320x240 coordinates. So for hi-res games the mask can maximum be half the background's resolution (there would be no point in having it larger since character coordinates are in 320x240 scale. At the moment, sprites that are too large will crash the game). However, you CAN use LOWER resolutions for the map to save disk space. The only rule is that it's dimensions must be proportional to your game resolution, so for an 800x600 game, the mask could be 400x300, 200x150 or even 100x75 - but the latter would only be recommended for very basic lighting.

Since the AGS color system only supports 64 different gray-scales, I had to add some tweaks to allow for the full 5-200% scaling range (previously the scaling jumped 6 points at a time). The module currently interpolates the scale from three (x,y) sets - the one from the previous frame, the current one and an estimation of the characters next position. It does create a bit of wobblyness when moving in-and-out of standing positions, so I'm trying to come up with alternate solutions.

The module is at the moment totally undocumented and does not confirm to AGS module guidelines. The only two functions available are:

LightMap.SetLightMap(int spriteid);
and
LightMap.SetScaleMap(int spriteid);

Typically you'll want to run these in on_event for eEventEnterRoomBeforeFadein, but to change a light-/scale-map during gameplay just call it again with a different sprite number. I haven't yet set up animation support but plan on it (so the player specifies a View instead of a sprite). You can also input 0 as spriteid and any light/-scale maps will be unloaded. The characters will be stuck with their current scale/color though.

*FILE LINK REMOVED DUE TO 2DADVENTURE SERVER PROBLEMS*

Edit: I should add that it's a good idea to paint your light/scale maps slightly larger than the walkable areas. Otherwise you míght see strange effects when moving near the edges.

Snarky

Instead of that complicated hack to interpolate between 64 levels of grayscale, wouldn't it be easier to use a wider spectrum of colors instead of just grayscale for the map, to provide more gradations? It should be relatively straightforward to automatically convert a 32-bit grayscale map to an 16-bit (or whatever) color map.

I'm thinking something like black->blue->cyan->white (i.e. first the B channel from min-max, then the G channel, then the R channel).

I've never played around with this functionality of AGS, so I'm just assuming that you can use the color channels this way.

GarageGothic

It should be possible, and it's definitely not a bad idea. But I see no user friendly way to implement it without a plugin or external converter - neither of which I'm able to code. While the concept COULD work for my in-game mask editor by drawing the mask to two separate surfaces (one for the actual mask, one for the grayscale image displayed to the player), the player would still only see the 64 grayscale levels while editing. You could possibly have a toggle key to show the full color image, but I doubt anyone would like to try editing that directly.

An alternate solution could be to fake more grayscale levels in the map editor by drawing some colors as semitransparent pixels (perhaps using a temporary DynamicSprite for the brush). But at the moment the idea makes my head hurt. I'll think about it some more.

I wonder if it would be possible to use the full RGB color spectrum if CJ implemented DrawingSurface.GetPixelRed, GetPixelBlue and GetPixelGreen values, or whether they still would be locked to the 16-bit range?

Snarky

I guess I was thinking more that you'd create the lightmap in Photoshop, and use a macro to convert it to the right format, and then import it into AGS. I wasn't really concerned with the limitations of an in-game editor.

GarageGothic

Yes, a Photoshop macro I could probably set up and distribute along with the module. Seems like the best solution would just be to add a "bool UseRGB" parameter to the SetScaleMask(int spriteid) function, so that non-Photoshop users and those wanting to use the in-game editor will have to settle for 64 tones.

Edit: Hmmm, wouldn't the black->blue->cyan->white method only allow for 32 more levels? While it's better than 64 I'm not sure it's worth the hassle. If it was set up to accept mixed colors (e.g. 128 red, 64 blue, 0 green)  we could have a hell of a lot of tones (32*32*32), but to set up this process in a Photoshop macro is becoming quite complicated.

Snarky

If I remember 16-bit RGB correctly, this method would give 32+64+32=128 levels (5+6+5 bits, since the green channel has an extra bit of precision... assuming AGS provides that bit of data).

In order to use more of the color space, as you say, and assuming we only have access to 15 bits, the easiest thing in Photoshop might be to use two of the channels as described, to provide 64 levels, and use the third channel as a multiplier to give us 128, 256, 512, ... or as many levels as we would like (up to some 32 thousand). It does become a little bit more tedious to create the macro, but I think 256 levels would still be very doable, and that's all you need, right?

Alternatively, it would not be too much work to write a small standalone app that calculates and converts the pixel values directly, for a more intuitive mapping (e.g. just read the integer value of each pixel). That way, people wouldn't need to have Photoshop, either. It's really the best solution. If you'd like to give this method a go, I could put together a simple C# app tonight or tomorrow.

GarageGothic

You could be right about green having 1 more bit (5+6+5=16, yeah :)), but I'm not sure how AGS handles it - if it does, that accuracy should already be available in the RGB code from the colormap module. However, that still only brings us up to 128 levels out of the 200 (or 195) needed, so I think your last solution makes most sense.

It would be awesome if you could write a bitmap color converter app. Just supply me with the arithmetic for the color encoding and I'll get it set up in my module. Thanks, man!

Snarky

Here's a quick command-line app for converting between 32-bit grayscale and 16-bit color color-spectrum maps. Warning: If you mess around with the command-line arguments it's possible to overwrite your input file(s), so back them up first!

The arithmetic is pretty simple: Given a 32-bit source image, it looks at the first byte of each pixel (the R channel), and whatever that number is, it puts it into the lower byte (the B channel and the lower 3 bits of the G channel) of the corresponding 16-bit target image. (Since x86 is little-endian, the lower byte is the first byte of each 16-bit byte-pair.)

To convert from the color mapping back to a 0-255 "grayscale" level, for each pixel you just calculate:

G * 32 + B


Err... or you could just use the GetPixel value directly. That ought to work. (If it doesn't, try dividing it by 256.)

Also, I would recommend that instead of trying to scale the 0-255 value we have to the 5-200 range we want, leave it as is and just chop it off at the boundaries.

GarageGothic

Great work Snarky, it does scale pixel perfect now! I tried passing the GetPixel value directly, and to my amazement it worked (I understand now that's how you coded the application, but I was expecting some kind of conversion necessary). Way cool.

However, I'm not sure about the multiplier. While the dark end of the spectrum works fine (scaling down to zero), I get light values going up to 479! Even though I can script around this, it puzzles me. Shouldn't it be 255?

Snarky

Glad it seems to be working. I'm able to read back a 0-255 range from the raw pixel data, so AGS must be doing something slightly different. I think I know why you're getting the result you're getting.

479 = 255 + 256 - 32

In other words, the maximum value of 9 bits with the 6th bit off. I would guess this has something to do with the green channel having 6 bits of precision instead of 5.
Desired             Actual AGS
interpretation:     interpretation:

Bits 87654321       9 87654321
Chan gggbbbbb       g gg-bbbbb
I think the easiest way to fix this is something like:

Code: ags

if(value>255)
  value = value - 224; // 224=256-32, i.e. b9-b6


No, wait... that's not right! That would almost certainly shuffle the bits around. Let's see...

Code: ags

value = (value / 64)*32 + (value % 32); // Do not simplify! Dividing by 64 is an important step here


I would certainly hope that one of these versions would work. Both should give you values in the 0-255 range, but only the right one will provide a smooth gradient without discontinuities.

GarageGothic

Yes, that last one seemed to do the trick. Works perfectly now. Thanks!

I think you're right that there's no need to alter the 0-255 range. Any "resolution gain" would be pure illusion since we already have the full range of scaling values available. And it's much more convenient for the user just to be able to put RGB(scale,scale,scale) as the color rather than having to figure out some arbitrary conversion system.

I'll keep working on the in-game editor. While it won't create smooth transitions, it's a good way for the player to prototype the base scale for each screen area while watching the character move around. It can then be saved to a .bmp/.pcx and opened in Photoshop to add smoothing and gradients.

Snarky

In case someone needs the details of the conversion, here's the relevant C# source code:

Code: ags

// This code, written by me, is public domain and can be used by anyone for any purpose.
// Parts of this code is adapted from a code example found at:
// http://msdn.microsoft.com/en-us/library/system.drawing.imaging.imagelockmode.aspx

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace LightmapConverter
{
	// Converts between 16-bit color-spectrum encoded maps and 32-bit grayscale-encoded maps
	// Because .NET converts 16-bit images to 32 bits internally, which would mess things up, we have to read and write the raw data ourselves
	class Converter
	{
		private PixelFormat format16bit;	// Whether we use 5 or 6 bits for the G channel (RGB:555 or RGB:565 format) in 16-bit

		public Converter(PixelFormat format16bit)
		{
			if (format16bit != PixelFormat.Format16bppRgb555 && format16bit != PixelFormat.Format16bppRgb565)
				throw new Exception("Illegal pixel format: Must be a 16-bit format");
			else
				this.format16bit = format16bit;
		}

		// Convert a 16-bit color-spectrum-encoded map to a 32-bit grayscale-encoded map
		public Bitmap ConvertToGrayscale(Bitmap colorImage)
		{
			// First, copy the image into a byte-array so we can read the raw data directly:

			// Lock the bitmap's bits.  
			Rectangle rect = new Rectangle(0, 0, colorImage.Width, colorImage.Height);
			System.Drawing.Imaging.BitmapData imageData =
				colorImage.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
				colorImage.PixelFormat);

			// Get the address of the first line.
			IntPtr colorImagePtr = imageData.Scan0;

			// Declare an array to hold the bytes of the bitmap.
			int arraySize = colorImage.Width*colorImage.Height*2;	// 2 bytes per pixel
			byte[] rawData = new byte[arraySize];

			// Copy the RGB values into the array.
			System.Runtime.InteropServices.Marshal.Copy(colorImagePtr, rawData, 0, arraySize);

			// Unlock the bits.
			colorImage.UnlockBits(imageData);

			// Now, read the raw data and write it to our new bitmap
			Bitmap grayscaleImage = new Bitmap(colorImage.Width, colorImage.Height, PixelFormat.Format32bppArgb);

			int index = 0;
			for (int y = 0; y < grayscaleImage.Height; y++)
			{
				for (int x = 0; x < grayscaleImage.Width; x++)
				{
					int value = rawData[index++];
					index++;			// We only use every other byte, 1 byte per 16-bit pixel

					Color grayLevel = Color.FromArgb(value, value, value);
					grayscaleImage.SetPixel(x, y, grayLevel);
				}
			}

			return grayscaleImage;
		}

		
		// Convert a 32-bit grayscale-encoded map to a 16-bit color-spectrum-encoded map
		public Bitmap ConvertToColor(Bitmap grayscaleImage)
		{
			// Create a byte array for the raw data
			int arraySize = grayscaleImage.Width * grayscaleImage.Height * 2;
			byte[] rawData = new byte[arraySize];

			// Write the data into the array in the format we want:
			//
			//         LOW BYTE HIGH BYTE  LOW BYTE HIGH BYTE ...
			// Bit:    87654321  87654321
			// RGB555: gggbbbbb  -rrrrrgg
			// RGB565: gggbbbbb  rrrrrggg
			//
			// Coding: vvvvvvvv  00000000
			//
			// Where v is the 8-bit grayscale value, and r,g,b are how the various bits map onto 16-bit color channels
			// under typical formats (top bit unused in RGB:555) (cf. http://msdn.microsoft.com/en-us/library/ms788140(VS.85).aspx).
			// (It doesn't particularly matter to us, though, as we ignore this RGB coding and just read/write the raw data.)
			//
			int index=0;
			for (int y = 0; y < grayscaleImage.Height; y++)
			{
				for (int x = 0; x < grayscaleImage.Width; x++)
				{
					rawData[index++] = grayscaleImage.GetPixel(x, y).R;	// If image is grayscale, R=G=B = v
					index++;	// We only use one byte of data per 16-bit pixel
				}
			}

			// Create a new bitmap and copy our raw data into it
			Bitmap colorImage = new Bitmap(grayscaleImage.Width, grayscaleImage.Height, format16bit);

			Rectangle rect = new Rectangle(0, 0, colorImage.Width, colorImage.Height);
			System.Drawing.Imaging.BitmapData imageData =
				colorImage.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
				colorImage.PixelFormat);
			IntPtr colorImagePtr = imageData.Scan0;

			System.Runtime.InteropServices.Marshal.Copy(rawData, 0, colorImagePtr, arraySize);

			colorImage.UnlockBits(imageData);

			return colorImage;	// Don't save this as a PNG file, since .NET does not support 16-bit PNGs and will convert it to 32-bit!
		}
	}
}

SMF spam blocked by CleanTalk