PAL Consoles and Colour

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

JetSetIlly

I've been working on NTSC colour generation for gopher2600. In other words, using the HUE-LUM nibbles used in the console's colour registers, create the YIQ signal.

Converting the HUE to the I and Q chroma values can be done by mapping the HUE value to a colour wheel:

   
phi = hue * 360 / (15 * adjust)   
The adjust is a value representing the colour adjustment potentiometer in the console.

We also need to adjust for the colour burst value.

   
phi += colourBurst
I and Q can then be derived:

   
I = Y * math.Cos(phi * degToRad)
Q = Y * math.Sin(phi * degToRad)
   
Y is the luminance value derived from the LUM nibble, the details of which aren't important here.

For display, the RGB values can then be extracted from the YIQ information.

So, the NTSC palette created without ever explicitely specifying the RGB values, which is what I want.



My question, is about how the PAL consoles work in this regard.

PAL encodes colour using YUV so I've started off in a similar way by mapping the HUE value to a colour wheel and then converting the phi value to U and V.

However, converting the YUV values to RGB values does not reproduce the expected PAL palette. I can get close but it's definitely not correct.

I think my mistake is in assuming that the HUE values correspond to equally spaced positions on the colour wheel.

So my question is this: do PAL consoles map the HUE values to UV values explicitly, rather than dividing a colour wheel like the NTSC consoles? Or is there a non-obvious way of performing the division?
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Andrew Davie

I run the Narrow Band Television Association forum - mostly dedicated to mechanical and early electronic television. On that forum are a number of extremely knowledgable television engineers who were involved in such stuff in the '60s or so. I suggest a visit and asking questions there!

NBTVA Forum

JetSetIlly

Quote from: Andrew Davie on 26 Nov 2024, 08:54 PMI run the Narrow Band Television Association forum - mostly dedicated to mechanical and early electronic television. On that forum are a number of extremely knowledgable television engineers who were involved in such stuff in the '60s or so. I suggest a visit and asking questions there!

NBTVA Forum


Excellent. Emulation of the television is at least as important as the console itself and I have many questions about CRTs

I could be wrong but I think this question is more about how the PAL TIA works rather than the TV itself. ie. how the U and V values are generated from the HUE nibble. That happens before the TV is involved.

But I'll prepare a post for the NBTVA forum all the same - there may be a pattern here I'm not seeing.
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social


Thomas Jentzsch

Did you have a look at Stella's code? IIRC it creates both NTSC and PAL palettes.

JetSetIlly

Quote from: Thomas Jentzsch on 27 Nov 2024, 03:30 AMDid you have a look at Stella's code? IIRC it creates both NTSC and PAL palettes.

I've dug in and I've found the trick. The calculation for V is slightly different between odd and even hue values. I'd never have found that on my own.

It still needs a bit of tuning but it's basically correct I think.

Thanks.


https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Thomas Jentzsch

Quote from: JetSetIlly on 27 Nov 2024, 08:29 AM
Quote from: Thomas Jentzsch on 27 Nov 2024, 03:30 AMDid you have a look at Stella's code? IIRC it creates both NTSC and PAL palettes.

I've dug in and I've found the trick. The calculation for V is slightly different between odd and even hue values. I'd never have found that on my own.
You're welcome. If you find any problems in my code, please let me know.

JetSetIlly

Quote from: Thomas Jentzsch on 27 Nov 2024, 10:00 AMYou're welcome. If you find any problems in my code, please let me know.

Where else in the pipeline do you adjust the gamma besides the part where you generate the Palette?

line 485 of PaletteHandler.cxx for NTSC and line 538 for PAL

You seem to be using 0.9 for NTSC and 1.2 for PAL. Where I believe the correct values should be 2.2 and 2.8. Your values would normally result in colours that are too light.

Or are those adjustments for some other non-gamma related reason?
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Thomas Jentzsch

#8
Quote from: JetSetIlly on 27 Nov 2024, 10:37 PM
Quote from: Thomas Jentzsch on 27 Nov 2024, 10:00 AMYou're welcome. If you find any problems in my code, please let me know.

Where else in the pipeline do you adjust the gamma besides the part where you generate the Palette?
In PaletteHander::adjustedPalette, here the user settings from e.g. gamma (myGamma) are applied.

QuoteYou seem to be using 0.9 for NTSC and 1.2 for PAL. Where I believe the correct values should be 2.2 and 2.8.

I remember that I searched for the correct gammas back then. This comment is based on these findings:
/* match common PC's 2.2 gamma to TV's 2.65 gamma */


Where did you find your gammas?

BTW: Macs have different gammas.  :-X


JetSetIlly

#9
Quote from: Thomas Jentzsch on 28 Nov 2024, 01:32 AMWhere did you find your gammas?

It's the gamma encoded into the PAL signal. In the section on colorimetry https://en.wikipedia.org/wiki/PAL

edit: Better link here. In the section on "Standard Gammas": https://en.wikipedia.org/wiki/Gamma_correction
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

Thomas Jentzsch

Quote from: JetSetIlly on 28 Nov 2024, 01:57 AM
Quote from: Thomas Jentzsch on 28 Nov 2024, 01:32 AMWhere did you find your gammas?

It's the gamma encoded into the PAL signal. In the section on colorimetry https://en.wikipedia.org/wiki/PAL

edit: Better link here. In the section on "Standard Gammas": https://en.wikipedia.org/wiki/Gamma_correction
Thanks. I think I will have to go back to my code eventually.

JetSetIlly

I've been tweaking this and the attached screenshot is very close to my (unadjusted) PAL 2600Jr. The TV settings have previously been calibrated to standard test cards and I'm happy with it.

For the colour test ROM the blues and purples could do with some work but they're pretty good. While some of the colours look the same they are different just not by much and maybe the difference is less then it should be.

This is the heart of the colour generation code for PAL

    // saturuation of chroma in final colour
    const chromaGain = 0.25

    // create UV from hue
    var U, V float64

    // even-numbered hue numbers go in the opposite direction for some reason
    if hue&0x01 == 0x01 {
        // green to lilac
        h := float64(hue)
        U = Y * chromaGain * math.Sin(burst-(fixed*h))
        V = Y * chromaGain * math.Sin(burst-(adjust*h*Y))
    } else {
        // gold to purple
        h := float64(hue + 2)
        U = Y * chromaGain * math.Sin(burst-(fixed*h))
        V = Y * chromaGain * -math.Sin(burst-(adjust*float64(hue)*Y))
    }

The angles used for the colour burst etc. are currently as follows:

    PALBurst = -47.56
    PALFixed = 17.31
    PALAdjust = -4.15

Y is in the range 0.2 to 0.85 for coloured hues and 0.075 to 1.0 for grey hues

None of this is set in stone yet but I don't think it's far from the truth.




https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

JetSetIlly

Decathalon is a good yardstick because the athlete's leg can look very yellow. For some reason, Activision used a different skin tone for the head and for the legs. $4a and $4e respectively.

The two areas look different on the hardware console is undeniable and this might be a clue as to how Activision had their PAL consoles/TV calibrated.

The screenshot is very similar to what I see from the hardware.
https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

JetSetIlly

I revisited the maths for this and managed to simplify it greatly. I've very happy with this now, although my phase adjustment seems to behave slightly differently to how Stella's phase adjustment works. I'll have to open my 2600 to play with it to see how it behaves it actuality.

func generatePAL(col signal.ColorSignal) color.RGBA {
    if col == signal.VideoBlack {
        return VideoBlack
    }

    // col is a colour-luminance value as stored internally in the 2600

    // color-luminance components of color signal
    lum := (col & 0x0e) >> 1
    hue := (col & 0xf0) >> 4

    // the min/max values for the Y component of greyscale hues
    const (
        minY = 0.35
        maxY = 1.00
    )

    // Y value in the range minY to MaxY based on the lum value
    Y := minY + (float64(lum)/8)*(maxY-minY)

    // PAL creates a grayscale for hues 0, 1, 14 and 15
    if hue <= 0x01 || hue >= 0x0e {
        if lum == 0x00 {
            // black is defined as 0% luminance, the same as for when VBLANK is
            // enabled
            //
            // some RGB mods for the 2600 produce a non-zero black value. for
            // example, the CyberTech AV mod produces a black with a value of 0.075
            return color.RGBA{A: 255}
        }
        g := uint8(Y * 255)
        return color.RGBA{R: g, G: g, B: g, A: 255}
    }

    var phiHue float64

    // even-numbered hue numbers go in the opposite direction for some reason
    if hue&0x01 == 0x01 {
        // green to lilac
        phiHue = float64(hue) * -PALPhase
    } else {
        // gold to purple
        phiHue = (float64(hue) - 2) * PALPhase
    }

    // angle of the colour burst reference is 180 by defintion
    const phiBurst = 180

    // see comments in generateNTSC for why we apply the adjusment and burst value to the
    // calculated phi
    const phiAdj = -57.28
    phiHue += phiAdj
    phi := phiHue + phiBurst

    // phi has been calculated in degrees but the math functions require radians
    phi *= math.Pi / 180

    // saturation of chroma in final colour. value currently uncertain
    const saturation = 0.3

    // create UV from hue
    U := Y * saturation * -math.Sin(phi)
    V := Y * saturation * -math.Cos(phi)

    // YUV to RGB conversion
    //
    // YUV conversion values taken from the "SDTV with BT.470" section of:
    // https://en.wikipedia.org/w/index.php?title=Y%E2%80%B2UV&oldid=1249546174
    R := clamp(Y + (1.140 * V))
    G := clamp(Y - (0.395 * U) - (0.581 * V))
    B := clamp(Y + (2.033 * U))

    return color.RGBA{
        R: uint8(R * 255.0),
        G: uint8(G * 255.0),
        B: uint8(B * 255.0),
        A: 255,
    }
}

The single adjustment for the phase (difference between hues) is currently 16.35 degrees

https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social

JetSetIlly

https://github.com/JetSetIlly/Gopher2600
@JetSetIlly@mastodon.gamedev.place
@jetsetilly.bsky.social