For a 3-dimensional lookup (input is (x,y), output is z) you want to do a bi-linear interpolation in both directions to smooth the output.

In the example below, I use indexes that range from 0x00 to 0x8f (columns) and 0x00 to 0xbf (rows). The indices can be considered as 4.4 fixed-point values, where the upper nibble is the “integral” value (so 0x0 to 0x8 for cols, 0x0 to 0xb for rows) that is used to index into the table.

The lower nibble forms the fractional part used for the interpolation.

As a 2D example, suppose you had a single-row table such as:

```
//col # 0x 1x 2x 3x 4x 5x 6x 7x 8x 9x Ax Bx Cx
const byte tbl[] = 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60
________________________________________________| |
| |
35 40----
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
y0 y1 y2 y3 y4 y5 y6 y7 y8 y9 yA yB yC yD yE yF
^^
Let idx = 0x7A -----------------
Index = 0x7 (0x7A >> 4)
Frac = 0xA (0x7A & 0x0f)
tbl[Index] = 35
tbl[Index+1] = 40
value = 35 + (40-35) * Frac/16
value = 35 + 5 * 10/16
value = 35 + 3.125
value = 38 (fractional part of fraction ignored)
```

In the 3D example, you just add additional stages of interpolation for the added dimension.

re the fraction, you can include it if you like, by checking the result of (p11 - p01)*fraccol (e.g.); if bit 3 is set (so the lower nibble is >= 0x80), you can round up the high-nibble if it’s <0xf_. Requires a bit more work but adds accuracy…)

This example using your table doesn’t include the fractional round-up but it can be added if you want:

```
const byte rpot= A0;
const byte lpot= A1;
const byte ledred = 9;
byte VE[][10] =
{ // col
//0x 1x 2x 3x 4x 5x 6x 7x 8x 90
{ 20, 20, 20, 23, 26, 31, 32, 35, 39, 40 }, // 0x
{ 25, 25, 25, 29, 32, 38, 47, 50, 54, 59 }, // 1x
{ 30, 30, 30, 33, 45, 50, 55, 59, 62, 65 }, // 2x
{ 33, 33, 35, 37, 49, 56, 58, 69, 72, 75 }, // 3x
{ 40, 40, 41, 45, 53, 59, 60, 77, 79, 80 }, // 4x
{ 44, 44, 45, 53, 59, 61, 62, 77, 79, 83 }, // 5x
{ 45, 45, 47, 55, 62, 64, 65, 77, 79, 83 }, // 6x row
{ 48, 48, 50, 56, 68, 68, 69, 77, 79, 83 }, // 7x
{ 49, 49, 51, 60, 67, 70, 70, 77, 79, 83 }, // 8x
{ 49, 49, 51, 60, 67, 70, 70, 77, 79, 83 }, // 9x
{ 49, 49, 51, 60, 67, 70, 70, 77, 79, 83 }, // Ax
{ 49, 49, 51, 60, 67, 70, 70, 77, 79, 83 }, // Bx
{ 49, 49, 51, 60, 67, 70, 70, 77, 79, 83 } // C0
};
void setup()
{
Serial.begin( 9600 );
pinMode( rpot, INPUT );
pinMode( lpot, INPUT );
pinMode( ledred, OUTPUT );
}//setup
void loop()
{
#if 1
for( byte row=0; row<0xc0; row++ )
{
for( byte col=0; col<0x90; col++ )
{
byte val = map( bilinearInterpolate( row, col ), 0, 83, 0, 255 );
Serial.println( val );
analogWrite( ledred, val );
}//for
}//for
#else
int rval = map( analogRead( rpot ), 0, 1024, 0, 0xbf ); //rows from 0x00 to 0xbf
int lval = map( analogRead( lpot ), 1, 1024, 0, 0x8f ); //cols from 0x00 to 0x8f
//
int FVE = map( bilinearInterpolate( rval, lval ), 0, 0x83, 0, 0xff );
analogWrite( ledred, FVE );
delay(10);
#endif
}//loop
byte bilinearInterpolate( byte rowval, byte colval )
{
byte
idxrow, idxcol,
fracrow, fraccol,
p0, p1, pval,
p01, p11,
p00, p10;
//ensure table limits are not exceeded
if( rowval > 0xbf )
rowval = 0xbf;
if( colval > 0x8f )
colval = 0x8f;
//upper nibbles are indexes into the rows/cols
//lower nibbles allow 15-steps of interpolation in x and y directions
idxrow = rowval >> 4;
fracrow = rowval & 0xf;
//
idxcol = colval >> 4;
fraccol = colval & 0xf;
//get the four "corner" values from the table at (row, col) and (row+1, col+1)
p01 = VE[idxrow][idxcol];
p11 = VE[idxrow][idxcol+1];
p00 = VE[idxrow+1][idxcol];
p10 = VE[idxrow+1][idxcol+1];
//perform bilinear interpolation (integer math) within that square
p1 = p01 + (((p11 - p01)*fraccol)>>4);
p0 = p00 + (((p10 - p00)*fraccol)>>4);
pval = p1 + (((p0-p1)*fracrow)>>4);
//return 3rd dimension (Z value) from bilinear interp
return pval;
}//bilinearInterpolate
```

Note the “#if 1” in loop() allows checking the PWM with a scope and loops for testing. If you want to try the potentiometers, change the ‘1’ to ‘0’.

P.S. Not a mathematician so I probably messed something up. It seems to work on the scope. YMMV.

P.P.S. This example only works for positive right/down tables. If you need to, check for negative slopes and apply the appropriate changes as needed…