Skip to content

Instantly share code, notes, and snippets.

@Meshiest
Last active April 12, 2020 21:41
Show Gist options
  • Select an option

  • Save Meshiest/082be54cd6d047ef49c54805d2ca8427 to your computer and use it in GitHub Desktop.

Select an option

Save Meshiest/082be54cd6d047ef49c54805d2ca8427 to your computer and use it in GitHub Desktop.
Generate simplex noise from brick programs. Baseplate layout: https://i.imgur.com/kDt3fac.png Example output: https://i.imgur.com/XR5YMcO.png
<!DOCTYPE html>
<!--
Build Documentation
nothing above colored brick stack wavelength + amplitude see settings
this plate [ glow palette plate ] [ -- metal feature plate -- ] [ holo setting plate ]
[ holo gen plate ] [ ------------------------ plastic program plate ------------------------ ]
[ ------------ metal base plate ---------- ]
Palette consists of a stack of bricks. The height and position of the brick determines what color
part of the generation will be.
You can use spaced out small flowers instead of taller bricks to create gradients (https://i.imgur.com/veII7Dk.png)
Settings (go over holo plate over plastic plate)
Rook - height determines extra size of generation
Knight - generate flat map
Bishop - height determines island distance
-->
<script src="https://cdn.jsdelivr.net/npm/brs-js/dist/dist.js"></script>
<script src="https://cdn.jsdelivr.net/gh/josephg/noisejs/perlin.js"></script>
<script src="https://gist.githack.com/Meshiest/2a253b88a514610772aa03625d41e23a/raw/bricktool.js"></script>
<!-- <script src="./tool.js"></script> -->
<!-- Files uploaded will be -->
<input id="fileInput" type="file">
<!-- This will be filled with the save object as JSON or the error message -->
<pre id="jsonElem"></pre>
<a id="anchor" download="autogen.brs">Download</a>
<script>
const author = {
id: 'e959baf3-731a-44df-9464-5b108f00a2f1',
name: 'Generator',
};
noise.seed(Math.random());
fileInput.addEventListener('change', e => {
const file = e.target.files[0];
if (file) {
file.arrayBuffer()
.then(buff => {
console.time('Reading');
const save = BRS.read(buff);
console.timeEnd('Reading');
console.log('Save:', save);
console.time('Parsing');
parseSave(save);
console.timeEnd('Parsing');
jsonElem.innerText = save.bricks.length + ' bricks';
})
.catch(err => {
// Display the error
jsonElem.innerText = 'Error: ' + err.message;
console.error(err)
});
}
});
function parseSave(save) {
const $ = $tool(save);
const parsePlate = plate => {
if (plate.position[2] !== 2)
return;
const plates = $.aboveBP(plate);
const outputPlates = $.find({asset: 'PB_DefaultBrick', material: 'BMC_Hologram', color: 0}, plates);
const programPlates = $.find({asset: 'PB_DefaultBrick', material: 'BMC_Plastic', color: 0}, plates);
// no render or program area
if (outputPlates.length !== 1 || programPlates.length !== 1)
return;
// stuff over the render zone
if ($.above(outputPlates[0]).length > 0)
return;
const program = $.aboveBP(programPlates[0]);
return {
plate,
output: outputPlates[0],
program,
};
};
// given a query, find bricks above those bricks
const $query = (q, target) => {
const res = $.find(q, target);
if (res.length > 0) {
const above = res.map($.above);
above.source = res;
return above;
}
return;
}
// get correct x axis from a rotated brick
const correctSize = brick =>
brick.rotation === 1 || brick.rotation == 3 ? [brick.size[1], brick.size[0], brick.size[2]] : brick.size;
// generate some terrain given a program
const generate = ({plate, output, program}) => {
const size = correctSize(output);
const tileSize = {x: 5, y: 5};
// determine generation dimensions
let width = size[0]/tileSize.x;
let height = size[1]/tileSize.y;
// offset for where the output is located
const topLeft = [
output.position[0] - size[0] + 5,
output.position[1] - size[1] + 5,
output.position[2] - size[2] + 4,
];
// default terrain gen has no color and mediocre terrain
let intervals = [{x: 1, ox: 0, y: 1, oy: 0, h: 0, z: 1}];
let settings = {};
// function to mask values
let masks = (x, y, v) => v;
let findColor = z => [[255, 255, 255, 255], 'BMC_Plastic'];
// these are the queries for program features
const intervalBricks = $query({material: 'BMC_Metallic'}, program);
const colorBricks = $query({material: 'BMC_Glow'}, program);
const settingsBricks = $query({material: 'BMC_Hologram'}, program);
const hasSetting = b => b && b.length === 1 && b[0].length > 0;
// if we have any intervals, extract the interval data from the brick
if (hasSetting(intervalBricks))
intervals = intervalBricks[0].map(b => {
const size = correctSize(b);
return {
x: size[0]/2, // x wavelength
ox: b.position[0], // x offset
y: size[1]/2, // y wavelength
oy: b.position[1], // y offset
z: size[2], // amplitude
h: b.position[2] - topLeft[2] - 4 - size[2], // z offset
}
});
// stack of bricks for ranges
if (hasSetting(colorBricks)) {
// generator tile size determined by palette plate size
const paletteSize = correctSize(colorBricks.source[0]);
tileSize.x = paletteSize[0];
tileSize.y = paletteSize[1];
// determine generation dimensions
width = size[0]/tileSize.x;
height = size[1]/tileSize.y;
// if lerp mode is enabled, use spaced out flowers to determine the color
const lerpMode = colorBricks[0][0].asset_name_index === $.findAsset('B_Small_Flower');
if (lerpMode) {
const colorRanges = colorBricks[0].map(b => ({
z: (b.position[2] - output.position[2] - 2) ,
color: typeof b.color === 'number' ? save.colors[b.color] : b.color,
material: b.material_index,
})).sort((a, b) => a.z - b.z);
findColor = z => {
// find first palette brick that is higher than given height
let index = colorRanges.findIndex(c => c.z > z);
if (index === -1)
index = colorRanges.length - 1;
// find the color before this one
let bottom = colorRanges[index - 1];
// upper bound
const top = colorRanges[index];
if (typeof bottom === 'undefined')
return [top.color, top.material];
const lerp = (a, b, p) => {
return a.map((v, i) => Math.min(255, Math.max(0, Math.round(v * p + b[i] * (1-p)))));
}
const p = (z-bottom.z)/(top.z-bottom.z);
return [lerp(top.color, bottom.color, p), top.material];
};
} else {
// use the size and position of the brick to determine range for this specific color
const colorRanges = colorBricks[0].map(b => ({
start: (b.position[2] - output.position[2] - 2) - b.size[2],
end: (b.position[2] - output.position[2] - 2) + b.size[2],
color: b.color,
material: b.material_index,
}));
// given a Z, locate the color from our palette
findColor = z => {
const range = colorRanges.find(r => r.start <= z && r.end >= z);
if (!range)
return [[255, 255, 255, 255], 'BMC_Plastic'];
return [range.color, range.material];
};
}
}
if (hasSetting(settingsBricks)) {
// size override setting
const rook = $.find({asset: 'B_Rook'}, settingsBricks[0]);
if (rook.length === 1) {
settings.sizeOverride = rook[0].position[2]/5;
// adjust new width and height
width = settings.sizeOverride;
height = settings.sizeOverride;
}
// island size
const bishop = $.find({asset: 'B_Bishop'}, settingsBricks[0]);
if (bishop.length === 1) {
// radius of island
const maxDist = bishop[0].position[2]/5;
// how soon to start sloping (based on darkness)
const gradient = save.colors[bishop[0].color].reduce((a, b) => a + b) / 1024;
masks = (x, y, v) => {
// distance from center
const dist = Math.hypot(x-width/2, y-height/2);
// as a scalar (0 - 1)
let scalar = dist/maxDist;
// make the transition more abrupt
scalar = Math.max(1 - gradient, scalar);
return (1-Math.min(1, Math.max(0, scalar))) * v;
};
}
// flat mode setting
const knight = $.find({asset: 'B_Knight'}, settingsBricks[0]);
if (knight.length === 1) {
settings.flatMode = true;
}
}
// using all of the bricks over our interval plate
// calculate a sum the waves together
const calcHeight = (x, y) =>
intervals.map(i =>
masks(x, y, noise.simplex2(
(x + i.ox) / i.x, // x wavelength + offset
(y + i.oy) / i.y, // y wavelength + offset
) * i.z + i.z) + i.h, // add in amplitude + z offset
)
.reduce((a, b) => a + b); // add the waves together
const bricks = [];
// build the grid of terrain bricks with very tall heights
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y ++) {
// neighbors are necessary for optimizing brick height
const neighbors = [[x-1,y],[x+1,y],[x,y-1],[x,y+1]] // adjacent neighbors positions
.map(([x, y]) => Math.round(calcHeight(x, y))); // get the expected brick height
// expected top of brick
let brickTop = Math.round(calcHeight(x, y));
// subtract the top from the shortest neighbor
let brickHeight = Math.min(Math.max(Math.round((brickTop - Math.min(...neighbors))/2), 2), 498);
// make it even
brickHeight += brickHeight % 2;
// find what part of the palette applies to this height
const [color, material] = findColor(brickTop);
// flat mode makes the entire output flat
if (settings.flatMode) {
brickTop = 4;
brickHeight = 2;
}
// add the brick to our generation output
bricks.push($.brick({
position: [x*tileSize.x*2, y*tileSize.y*2, brickTop - brickHeight],
size: [tileSize.x, tileSize.y, brickHeight],
color,
material,
}));
}
}
// move the bricks to the output location
return $.shifted(bricks, topLeft);
};
// all white metal platforms can have potential to be programs
const avail = $.find({asset: 'PB_DefaultBrick', material: 'BMC_Metallic', color: 0})
.map(parsePlate)
.filter(p => p);
// run all the programs
const bricks = [...avail.flatMap(generate)];
// save the bricks
$.write(bricks);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment