Something about this piqued my interest because I’ve had “more flexible mapping” on my to-do list for a while.
I started playing around with a few ideas over the weekend and now I’ve got something ready to share:
Within the limits of available memory, it supports remapping the set of available pixels into any number of regions of arbitrary size and shape, each with its own pattern. The example in the video is running:
Multiple Patterns in Multiple Regions
////////////////////////////////////////////////////////////////////////////////
//
// MULTIPLE PATTERNS IN MULTIPLE REGIONS: different patterns running in different user-defined regions.
//
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//
// Simple sample patterns. But you're not limited to these; size permitting, most
// freestanding patterns can be used within a custom region. To do so requires copying
// the pattern code into this section, renaming the pattern's "beforeRender()" and
// "render()" functions to something unique (like "beforeRender_{patternName}" and
// "render_{patternName}"), and defining the pixels to be contained within the region.
// If the pattern contains additional functions or variables outside the "beforeRender()"
// and "render()" functions, they may also need to be renamed to avoid collisions.
//
//-------------------------------------------------------------------------------------
// Demo pattern is @jeff's KITT from https://forum.electromage.com/t/kitt-without-arrays/1219
var t_KITT;
export function beforeRender_KITT() { t_KITT = time(.05) }
function pulse(x) {
var halfsaw = triangle(x - t_KITT) * square(x - t_KITT, .5);
var tailPct = .4, shift = 1 - tailPct;
return max(0, (halfsaw - shift) / tailPct)
}
export function render_KITT(index) {
pct = index / pixelCount / 2; v = pulse(pct) + pulse(-pct); hsv(0, 1, v * v * v)
}
//-------------------------------------------------------------------------------------
// @jeff's sinusoidal waves from https://forum.electromage.com/t/single-moving-pixel-across-a-2d-map/1324/4
export function beforeRender_SinusoidalWaves(delta) {
var basePeriod = 1.2 / 65.535
t1_SW = time(basePeriod); t2_SW = time(basePeriod * 1.05); t3_SW = time(basePeriod * 1.1);
}
// Returns 1 when a & b are proximate, 0 when they are more than `halfwidth`
// apart, and a gamma-corrected brightness for distances within `halfwidth`
function near(a, b, halfwidth) {
var halfwidthDefault = 0.6; if (halfwidth == 0) halfwidth = halfwidthDefault;
var v = clamp(1 - abs(a - b) / halfwidth, 0, 1);
return v * v * v;
}
export function render2D_SinusoidalWaves(index, x, y) {
x /= 2; y = 1.4 * y - .2;
r = near(y, wave(x - t1_SW)); g = near(y, wave(x - t2_SW)); b = near(y, wave(x - t3_SW));
rgb(r, g, b);
}
//-------------------------------------------------------------------------------------
// @zranger1's RADAR 2D
var dishAngle;
export function beforeRender_Radar(delta) {
dishAngle = time(.03); circleRadius = time(0.015);;
}
export function render2D_Radar(index, x, y) {
var beamWidth = 0.35;
x -= 0.5; y -=0.5;
v = (PI + atan2(x, y))/PI2;
d = 0.5-abs(0.5 - abs(dishAngle - v));
v = (d < beamWidth) ? 1-(d/beamWidth) : 0;
h = sqrt(x*x + y*y);
if (abs(h-circleRadius) < 0.05) { s = 0; v = 0.5 } else { s = 1; }
hsv(h, s, v*v*v*v);
}
//-------------------------------------------------------------------------------------
// '2D Red-Green Sweep' converted to Green-Blue
var axis, t1_RG;
export function beforeRender_RedGreenSweep(delta) {
t1_RG = time(.05);
axis = t1_RG > .5;
t1_RG *= 2;
}
export function render2D_RedGreenSweep(index, x, y) {
h = 1/3 + axis/3;
v = wave((axis ? y : x) / 2 - t1_RG + .5);
v = pow(v, 20);
hsv(h, 1, v);
}
////////////////////////////////////////////////////////////////////////////////
//
// The regions within this matrix.
//
// The "beforeRenderer" and "renderer" columns refer to the renamed "beforeRender()" and "render()"
// functions of the particular pattern to be run in this region. If the pattern doesn't have
// a render2D() or render3D() function, those fields should be set to zero.
//
// Regions can contain any number of pixels, in any shape, direction or order. The The pixel index
// numbers in the example below refer to a CJMCU 16-by-16 matrix with zigzag addressing:
// 0 31 32 63 64 95 96 127 128 159 160 191 192 223 224 255
// 1 30 33 62 65 94 97 126 129 158 161 190 193 222 225 254
// 2 29 34 61 66 93 98 125 130 157 162 189 194 221 226 253
// 3 28 35 60 67 92 99 124 131 156 163 188 195 220 227 252
// 4 27 36 59 68 91 100 123 132 155 164 187 196 219 228 251
// 5 26 37 58 69 90 101 122 133 154 165 186 197 218 229 250
// 6 25 38 57 70 89 102 121 134 153 166 185 198 217 230 249
// 7 24 39 56 71 88 103 120 135 152 167 184 199 216 231 248
// 8 23 40 55 72 87 104 119 136 151 168 183 200 215 232 247
// 9 22 41 54 73 86 105 118 137 150 169 182 201 214 233 246
// 10 21 42 53 74 85 106 117 138 149 170 181 202 213 234 245
// 11 20 43 52 75 84 107 116 139 148 171 180 203 212 235 244
// 12 19 44 51 76 83 108 115 140 147 172 179 204 211 236 243
// 13 18 45 50 77 82 109 114 141 146 173 178 205 210 237 242
// 14 17 46 49 78 81 110 113 142 145 174 177 206 209 238 241
// 15 16 47 48 79 80 111 112 143 144 175 176 207 208 239 240
//
var regions = [
// Each region is a 3-element array containing:
// - the name of the renamed beforeRender() function,
// - the name of the renamed render?D() function,
// - a nested array containing the index numbers of all the pixels in the region.
//
// A 1D region going around the outer edge of the matrix.
[ beforeRender_KITT, render_KITT, [
0, 31, 32, 63, 64, 95, 96, 127, 128, 159, 160, 191, 192, 223, 224, // top edge
255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, // right edge
240, 239, 208, 207, 176, 175, 144, 143, 112, 111, 80, 79, 48, 47, 16, // bottom edge
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // left edge
]
],
// A 2D region occupying the top half of the matrix (minus the outer edges).
[ beforeRender_SinusoidalWaves, render2D_SinusoidalWaves, [
30, 33, 62, 65, 94, 97, 126, 129, 158, 161, 190, 193, 222, 225,
29, 34, 61, 66, 93, 98, 125, 130, 157, 162, 189, 194, 221, 226,
28, 35, 60, 67, 92, 99, 124, 131, 156, 163, 188, 195, 220, 227,
27, 36, 59, 68, 91, 100, 123, 132, 155, 164, 187, 196, 219, 228,
26, 37, 58, 69, 90, 101, 122, 133, 154, 165, 186, 197, 218, 229,
25, 38, 57, 70, 89, 102, 121, 134, 153, 166, 185, 198, 217, 230,
]
],
// A 2D region occuping the left two-thirds of the bottom half (minus the outer edges).
[ beforeRender_Radar, render2D_Radar, [
24, 39, 56, 71, 88, 103, 120, 135,
23, 40, 55, 72, 87, 104, 119, 136,
22, 41, 54, 73, 86, 105, 118, 137,
21, 42, 53, 74, 85, 106, 117, 138,
20, 43, 52, 75, 84, 107, 116, 139,
19, 44, 51, 76, 83, 108, 115, 140,
18, 45, 50, 77, 82, 109, 114, 141,
17, 46, 49, 78, 81, 110, 113, 142,
]
],
// A 2D region occupying the right third of the bottom half (minus the outer edges).
[ beforeRender_RedGreenSweep, render2D_RedGreenSweep, [
152, 167, 184, 199, 216, 231,
151, 168, 183, 200, 215, 232,
150, 169, 182, 201, 214, 233,
149, 170, 181, 202, 213, 234,
148, 171, 180, 203, 212, 235,
147, 172, 179, 204, 211, 236,
146, 173, 178, 205, 210, 237,
145, 174, 177, 206, 209, 238
]
]
];
////////////////////////////////////////////////////////////////////////////////
//
// There's no need to edit anything below this line.
//
var pixelCountOriginal = pixelCount;
var whichRegion = array(pixelCountOriginal); // The region each pixel belongs to.
var whichIndex = array(pixelCountOriginal); // The relative index of each pixel within its region.
export var mapXYZ; // The map coordinates for each real pixel.
var numDimensions = 0;
// HELPER functions.
function allocateXYZ(dimensions) {
if (numDimensions == 0) {
numDimensions = dimensions;
mapXYZ = array(pixelCountOriginal * numDimensions);
}
}
function storeXYZ(index, x, y, z) {
if (numDimensions >= 1) mapXYZ[numDimensions*index+0] = x;
if (numDimensions >= 2) mapXYZ[numDimensions*index+1] = y;
if (numDimensions == 3) mapXYZ[numDimensions*index+2] = z;
}
function rescaleXYZ(realIndexArray) {
var minima = [1, 1, 1]; var maxima = [0, 0, 0];
// Find the highest and lowest values in this region.
for (var iterIndex = 0; iterIndex < realIndexArray.length; iterIndex++) {
var thisIndex = realIndexArray[iterIndex];
for (var iterDimension = 0; iterDimension < numDimensions; iterDimension++) {
var candidate = mapXYZ[thisIndex * numDimensions + iterDimension];
if (candidate < minima[iterDimension]) minima[iterDimension] = candidate;
if (candidate > maxima[iterDimension]) maxima[iterDimension] = candidate;
}
}
// Now make another pass through and rescale the values to 0..1. If someone is doing freaky things
// with irregular shapes it might not work, but for simple rectangles this should be fine.
for (iterIndex = 0; iterIndex < realIndexArray.length; iterIndex++) {
thisIndex = realIndexArray[iterIndex];
for (iterDimension = 0; iterDimension < numDimensions; iterDimension++) {
var span = maxima[iterDimension] - minima[iterDimension];
var value = mapXYZ[numDimensions*thisIndex+iterDimension];
value -= minima[iterDimension]; // offset
if (span > 0) value /= span; // rescale
mapXYZ[thisIndex * numDimensions + iterDimension] = clamp(value, 0, 1);
}
}
// For each pixel index in this region, store its region and its relative offset.
regions.forEach((v, i, a) => {
pixelCount = a[i][2].length;
for (var iterPixel = 0; iterPixel < pixelCount; iterPixel++) {
var whichPixel = a[i][2][iterPixel];
whichRegion[whichPixel] = i; whichIndex[whichPixel] = iterPixel;
}
});
}
// BEFORERENDER functions.
export var mapMode = -1; // Mode selector: 0 = capture coordinates; 1 = rescale coordinates; 2 = draw
export function beforeRender(delta) {
saveDelta = delta;
// for each client region:
regions.forEach((v, i, a) => {
// rescale the client map the first time through
if (mapMode == 1) rescaleXYZ(a[i][2]);
// call client beforeRenderer() functions.
pixelCount = a[i][2].length; if (a[i][0] != 0) a[i][0](saveDelta);
});
// Move to the next map-capture modes.
if (mapMode < 2) mapMode++;
}
// RENDER functions.
function renderRegions(index) {
// Get the appropriate region-specific coordinates for this pixel.
var offset = index*numDimensions;
var x = mapXYZ[offset+0];
var y = 0; if (numDimensions > 1) y = mapXYZ[offset+1];
var z = 0; if (numDimensions > 2) z = mapXYZ[offset+2];
// set the appropriate pixelCount for this region.
pixelCount = regions[whichRegion[index]][2].length;
// call client render?D() functions.
regions[whichRegion[index]][1](whichIndex[index], x, y, z);
}
export function render(index) {
// On the first pass, store the map coordinates. On successive passes, call the client renderer.
if (mapMode == 0) { allocateXYZ(1); storeXYZ(index, index/pixelCount, 0, 0); return; } else renderRegions(index);
}
export function render2D(index, x, y) {
// On the first pass, store the map coordinates. On successive passes, call the client renderer.
if (mapMode == 0) { allocateXYZ(2); storeXYZ(index, x, y, 0); return; } else renderRegions(index);
}
export function render3D(index, x, y, z) {
// On the first pass, store the map coordinates. On successive passes, call the client renderer.
if (mapMode == 0) { allocateXYZ(3); storeXYZ(index, x, y, 0); return; } else renderRegions(index);
}
All the housekeeping needed to multiplex, relocate and rescale each pattern causes about a 66% performance hit because PBscript arrays don’t have a findIndex()
or indexOf()
function, but the frame rate still seems acceptable for most computationally-light patterns. If you have any ideas for further optimizations, speak up (where’s @scruffynerf when you need him?).