Last active
April 12, 2020 21:41
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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