UNPKG

cellular-automata-voxel-shader

Version:

Generate a voxel shader (for MagicaVoxel) from a custom CA rule.

378 lines (326 loc) 13.1 kB
"use strict"; const uniq = require('uniq'); function printFloat (v) { return (v === v|0 ? v.toPrecision(2) : v.toString(10)); } function generateComment (what, rule, outOfBoundValue) { const comments = [ '/**', ' * ' + what + ' generated by cellular-automata-voxel-shader 0.4.0', ' * ', ' * Rule format : ' + rule.ruleFormat, ' * Rule : ' + rule.ruleString, ' * Out of bound value : ' + outOfBoundValue, ' */' ]; return comments.join('\n'); } function generateUniformsAndConstants () { return [ '// console command', '// \'xs -n [iterations] shadername\' : e.g. \'xs -n 20 shadername\'', '', '// shader inputs', '// uniform vec3 iVolumeSize; // volume size [1-126]', '// uniform float iColorIndex; // current color index [1-255]', '// uniform vec3 iMirror; // current mirror mode [0-1]', '// uniform vec3 iAxis; // current axis mode [0-1]', '// uniform float iFrame; // current frame', '// uniform float iNumFrames; // total number of frames', '// uniform float iIter; // current frame', '// uniform vec4 iRand; // random numbers', '// uniform float iArgs[8]; // user args', '', '// built-in functions', '// float voxel(vec3 v);' ].join('\n'); } function generateGetVoxelGlsl (outOfBoundValue) { outOfBoundValue = outOfBoundValue || 0; if (outOfBoundValue === 'wrap') { return [ 'int getVoxel(const in vec3 currentPos, const in vec3 add) {', ' vec3 voxelPos = mod((currentPos + add), iVolumeSize);', ' return int(voxel(voxelPos));', '}' ].join('\n'); } else if(outOfBoundValue === 'clamp') { return [ 'int getVoxel(const in vec3 currentPos, const in vec3 add) {', ' vec3 voxelPos = (currentPos + add) ;', ' voxelPos = clamp(voxelPos, vec3(0.), iVolumeSize - vec3(1.));', ' return int(voxel(voxelPos));', '}' ].join('\n'); } else { return [ 'int getVoxel(const in vec3 currentPos, const in vec3 add) {', ' vec3 voxelPos = (currentPos + add) ;', ' if(voxelPos.x < 0. || voxelPos.y < 0. || voxelPos.z < 0. || voxelPos.x >= iVolumeSize.x || voxelPos.y >= iVolumeSize.y || voxelPos.z >= iVolumeSize.z) {', ' return ' + outOfBoundValue + ';', ' } else {', ' return int(voxel(voxelPos));', ' }', '}' ].join('\n'); } } function generateGetNeighbourhood (neighbourhood) { const glsl = [ 'int getNeighbourhood (const in vec3 currentPos) {', ' int sum = 0;', '' ]; for (let i = 0; i < neighbourhood.length; i++) { const neighbour = neighbourhood[i]; glsl.push(' sum += getVoxel(currentPos, vec3(' + printFloat(neighbour[0]) + ', ' + printFloat(neighbour[1]) + ', ' + printFloat(neighbour[2]) + ')) > 0 ? 1 : 0;'); } glsl.push('', ' return sum;', '}'); return glsl.join('\n'); } function generateGetNeighbourhoodCond (neighbourhood) { const glsl = [ 'int getNeighbourhoodCond (const in vec3 currentPos, const in int desiredValue) {', ' int sum = 0;', '' ]; for (let i = 0; i < neighbourhood.length; i++) { const neighbour = neighbourhood[i]; glsl.push(' sum += getVoxel(currentPos, vec3(' + printFloat(neighbour[0]) + ', ' + printFloat(neighbour[1]) + ', ' + printFloat(neighbour[2]) + ')) == desiredValue ? 1 : 0;'); } glsl.push('', ' return sum;', '}'); return glsl.join('\n'); } function generateRandomFunction () { return [ 'float rand(vec3 co, float seed) {', ' co = co + vec3(fract(sin(dot(vec2(iRand.x * 4. * 5.9898, seed * 78.5453), vec2(12.9898,78.233))) * 43758.5453));', ' return fract(sin(dot(co.xy + vec2(length(co.yz) * 24.0316), vec2(12.9898,78.233)) + dot(co.yz + vec2(length(co.zx) * 24.0316), vec2(12.9898,78.233)) + dot(co.zx + vec2(length(co.xy) * 24.0316), vec2(12.9898,78.233))) * 43758.5453);', '}' ].join('\n'); } function compareNumbers (a, b) { return a - b; } function generateEqualityCheck (values, variable) { const checkString = []; const groupedValues = []; let previousValue = null; variable = variable || 'sum'; if (values && values.length) { uniq(values, compareNumbers, true); for (let i = 0; i < values.length; i++) { if (previousValue === values[i] - 1) { groupedValues[groupedValues.length - 1].push(values[i]); } else { groupedValues.push([values[i]]); } previousValue = values[i]; } for (let i = 0; i < groupedValues.length; i++) { if (groupedValues[i].length > 1) { checkString.push('(' + variable + ' >= ' + groupedValues[i][0] + ' && ' + variable + ' <= ' + groupedValues[i][groupedValues[i].length - 1] + ')'); } else { checkString.push(variable + ' == ' + groupedValues[i][0]); } } } else { checkString.push('false'); } return checkString.length > 1 ? '(' + checkString.join(' || ') + ')' : checkString[0]; } function generateProbabilityCheck (probabilities, sumVariable, positionVariable) { const checkString = []; const groupedValues = []; const groupProbabilities = []; let previousValue = null; let previousProbability = null; sumVariable = sumVariable || 'sum'; positionVariable = positionVariable || 'position'; for (let i in probabilities) { const value = parseInt(i, 10); const probability = probabilities[i]; if (previousValue === value - 1 && previousProbability === probability) { groupedValues[groupedValues.length - 1].push(value); } else { groupedValues.push([value]); groupProbabilities.push(probability); } previousValue = value; previousProbability = probability; } for (let i = 0; i < groupProbabilities.length; i++) { const probability = groupProbabilities[i]; if (probability === 1) { if (groupedValues[i].length > 1) { checkString.push('(' + sumVariable + ' >= ' + groupedValues[i][0] + ' && ' + sumVariable + ' <= ' + groupedValues[i][groupedValues[i].length - 1] + ')'); } else { checkString.push(sumVariable + ' == ' + groupedValues[i][0]); } } else if (probability > 0) { if (groupedValues[i].length > 1) { checkString.push('(' + sumVariable + ' >= ' + groupedValues[i][0] + ' && ' + sumVariable + ' <= ' + groupedValues[i][groupedValues[i].length - 1] + ' && rand(' + positionVariable + ', 1.) < ' + probability + ')'); } else { checkString.push('(' + sumVariable + ' == ' + groupedValues[i][0] + ' && rand(' + positionVariable + ', 1.) < ' + probability + ')'); } } } return checkString.length > 1 ? '(' + checkString.join(' || ') + ')' : checkString[0]; } function generateProcessGlslGenerations (neighbourhood, stateCount, survival, birth) { const glsl = [ generateGetNeighbourhoodCond(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int sum = getNeighbourhoodCond(position, 1);', ' if (currentValue == 0 && ' + generateEqualityCheck(birth) + ') {', ' return 1;', ' } else if (currentValue == 1 && ' + generateEqualityCheck(survival) + ') {', ' return 1;', ' } else if (currentValue > 0) {', ' return int(mod(float(currentValue + 1), ' + printFloat(stateCount) + '));', ' }', ' return 0;', '}' ]; return glsl.join('\n'); } function generateProcessGlslLife (neighbourhood, survival, birth) { const glsl = [ generateGetNeighbourhood(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int sum = getNeighbourhood(position);', ' float sumAdd = clamp(iArgs[0], 0., 1.);', ' if (currentValue == 0 && ' + generateEqualityCheck(birth) + ') {', ' return 1 + int(float(sum) * sumAdd);', ' } else if (currentValue > 0 && ' + generateEqualityCheck(survival) + ') {', ' return 1 + int(float(sum) * sumAdd);', ' }', ' return 0;', '}' ]; return glsl.join('\n'); } function generateProcessGlslStochastic (neighbourhood, survival, birth) { const glsl = [ generateRandomFunction(), '', generateGetNeighbourhood(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int sum = getNeighbourhood(position);', ' float sumAdd = clamp(iArgs[0], 0., 1.);', ' if (currentValue == 0 && ' + generateProbabilityCheck(birth) + ') {', ' return 1 + int(float(sum) * sumAdd);', ' } else if (currentValue > 0 && ' + generateProbabilityCheck(survival) + ') {', ' return 1 + int(float(sum) * sumAdd);', ' }', ' return 0;', '}' ]; return glsl.join('\n'); } function generateProcessGlslVote (neighbourhood, votes) { const glsl = [ generateGetNeighbourhood(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int sum = getNeighbourhood(position) + (currentValue > 0 ? 1 : 0);', ' float sumAdd = clamp(iArgs[0], 0., 1.);', ' if (' + generateEqualityCheck(votes) + ') {', ' return 1 + int(float(sum) * sumAdd);', ' }', ' return 0;', '}' ]; return glsl.join('\n'); } function generateProcessGlslLuky (neighbourhood, lowSurvival, highSurvival, lowBirth, highBirth) { const glsl = [ generateGetNeighbourhood(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int sum = getNeighbourhood(position);', ' float sumAdd = clamp(iArgs[0], 0., 1.);', ' if (currentValue == 0 && sum >= ' + lowBirth + ' && sum <= ' + highBirth + ') {', ' return 1 + int(float(sum) * sumAdd);', ' } else if (currentValue > 0 && sum >= ' + lowSurvival + ' && sum <= ' + highSurvival + ') {', ' return 1 + int(float(sum) * sumAdd);', ' }', ' return 0;', '}' ]; return glsl.join('\n'); } function generateProcessGlslNluky (neighbourhood, stateCount, lowSurvival, highSurvival, lowBirth, highBirth) { const glsl = [ generateGetNeighbourhoodCond(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int sum = getNeighbourhoodCond(position, 1);', ' if (currentValue == 0 && sum >= ' + lowBirth + ' && sum <= ' + highBirth + ') {', ' return 1;', ' } else if (currentValue == 1 && sum >= ' + lowSurvival + ' && sum <= ' + highSurvival + ') {', ' return 1;', ' } else if (currentValue == 1) {', ' return ' + (2 % (2 + stateCount * 2)) + ';', ' } else if (currentValue >= 2) {', ' return int(mod(float(currentValue + 2), ' + printFloat(2 + stateCount * 2) + '));', ' }', ' return 0;', '}' ]; return glsl.join('\n'); } function generateProcessGlslCyclic (neighbourhood, stateCount, threshold, greenbergHastingsModel) { const glsl = [ generateGetNeighbourhoodCond(neighbourhood), '', 'int process(const in int currentValue, const in vec3 position) {', ' int nextValue = int(mod(float(currentValue + 1), ' + printFloat(stateCount) + '));', ' int sum = getNeighbourhoodCond(position, nextValue);', ' if (sum >= ' + threshold + (greenbergHastingsModel ? ' || currentValue > 0' : '') + ') {', ' return nextValue;', ' }', ' return currentValue;', '}' ]; return glsl.join('\n'); } function generateProcessGlsl (neighbourhood, rule) { if (rule.ruleFormat === 'life' || rule.ruleFormat === 'extended-life') { return generateProcessGlslLife(neighbourhood, rule.survival, rule.birth); } else if (rule.ruleFormat === 'extended-stochastic') { return generateProcessGlslStochastic(neighbourhood, rule.survival, rule.birth); } else if (rule.ruleFormat === 'generations' || rule.ruleFormat === 'extended-generations') { return generateProcessGlslGenerations(neighbourhood, rule.stateCount, rule.survival, rule.birth); } else if (rule.ruleFormat === 'vote') { return generateProcessGlslVote(neighbourhood, rule.vote); } else if (rule.ruleFormat === 'luky') { return generateProcessGlslLuky(neighbourhood, rule.lowSurvival, rule.highSurvival, rule.lowBirth, rule.highBirth); } else if (rule.ruleFormat === 'nluky') { return generateProcessGlslNluky(neighbourhood, rule.stateCount, rule.lowSurvival, rule.highSurvival, rule.lowBirth, rule.highBirth); } else if (rule.ruleFormat === 'cyclic') { return generateProcessGlslCyclic(neighbourhood, rule.stateCount, rule.threshold, rule.greenbergHastingsModel); } throw new Error('Unsupported ruleFormat : ' + rule.ruleFormat); } function generateGlsl (rule, neighbourhood, outOfBoundValue) { const glsl = [ generateComment('Voxel Shader for MagicaVoxel 0.98.2', rule, outOfBoundValue), '', generateUniformsAndConstants(), '', generateGetVoxelGlsl(outOfBoundValue), '', generateProcessGlsl(neighbourhood, rule), '', 'float map(vec3 v) {', ' int currentValue = int(voxel(v));', ' return float(process(currentValue, v));', '}' ]; return glsl.join('\n') } module.exports = generateGlsl;