Terrain generation is based on a coherent noise function invented by Cube which is referred to as 'Factorio basis noise'.
Sampling a single factorio basis noise function results in a uniform bumpy texture. More interesting textures can be defined by adding together multiple iterations of the basis noise at different scales to make 'multi-octave noise'. Small scales (high frequency, low amplitude) create small features (if we're talking about elevation, this means puddles) and large scales (low frequency, high amplitude) create large features (mountains).
And everybody had a great day.
But if you hang around multi-octave noise long enough, it starts to look repetetive, too. There will be big things and little things, but all the big things look pretty much alike and all the little things look pretty much alike.
This can be solved by:
-
Using a less uniform basis function
-
Combining functions in ways more complicated than by adding them together
In 0.15 and earlier, the conventional way to make slightly more interesting noise is to change the 'persistence' using another noise function. Persistence is the amplitude of a given octave in relation to the next 'larger' one. High persistence makes higher frequency noise more prominent.
Different map properties (elevation, temperature, humidity, aux, and one-off anonymous layers indicated by autoplace settings) are defined by different 'noise layers', each of which, up through 0.15, is defined by
- seed
- highest frequency
- amplitude of highest frequency
- octave count
- persistence
Those parameters could be adjusted by prototypes and map settings, but anything more complex required special case C++-level logic in whatever part of the code dealt with that layer. e.g. elevation's persistence was varied across the map based on another multi-octave noise layer, and the code to implement this was in TilePropertiesProvider.cpp.
The programmable noise system replaces the specification of a noise function by those 5 parameters with a functional language.
Noise functions are treated as prototypes and composed from Lua, so mods can add new ones. See data/core/prototypes/noise-programs.lua for working examples.
You'll notice that noise expressions are defined using code like
expression = noise.define_noise_function( function(x,y,tile,map)
...bunch of stuff here...
end)
For example, a multi-octave noise function could be defined in terms of the basis noise function (assuming a simplified 'basis_noise' function has been defined that takes seed, x, and y parameters):
expression = noise.define_noise_function(function(x,y,tile,map)
return basis_noise(123, x, y) + basis_noise(456, x/2, y/2)*2 + basis_noise(789, x/3, y/3)*3
end)
The above is valid Lua code, but the Lua interpreter is not actually involved in calculating noise,
since that would be slow.
Instead, noise expression objects are passed around
(in the above example, x
, y
, and the return value from basis_noise(...)
)
as value placeholders and arithmetic operators on them are overridden
to produce placeholders for the results of those operations.
The result of evaluating the above function, then, is a data structure representing the composed noise function, which would look something like this when represented in Lua syntax:
{
type = "function-application",
function_name = "add",
arguments =
{
{
type = "function-application",
function_name = "factorio-basis-noise",
arguments =
{
x =
{
type = "variable"
variable_name = "x",
},
y =
{
type = "variable"
variable_name = "x",
},
seed0 =
{
type = "literal-number",
literal_value = 123
}
}
},
{
...multiply 2 by an application of factorio-basis-noise
with arguments that are themselves x and y divided by 2...
},
{
...multiply 3 by an application of factorio-basis-noise
with arguments that are themselves x and y divided by 3...
},
}
}
This structure is compiled into a list of operations that can be efficiently executed while generating new sections of map. Notably, each part of the expression is evaluated for multiple samples at a time, to amortize the cost of interpretation.
If you wanted,
you could just write that data structure as a literal into your noise expression prototype definition
instead of using noise.define_noise_function
.
That would be much more verbose, but maybe useful if you wanted to write your own compiler.
Every expression and subexpression is represented as an object with a type
property,
which is a string naming the type of the expression.
There are 3 basic expression types:
variable
, which has avariable_name
referencing a predefined variable or constantliteral-number
, which has aliteral_value
that is the expression's valuefunction-application
, which has afunction_name
and array ofarguments
(some functions take a integer-indexed list of arguments, while more complex ones take named arguments)
See MapGenSettings::initNoiseProgram to see what variables and constants are available.
Some available functions (defined by an if-else chain in NoiseProgramBuilder::_compileExpression):
add
( a, b )subtract
( minuend, subtrahend )multiply
( a, b )divide
( dividend, divisor )exponentiate
( base, exponent )clamp
( value, minimum, maximum )ridge
( value, minimum, maximum ) - folds value back as many times as necessary to fit it within the given rangefactorio-basis-noise
( ... ) - sample a layer of the factorio basis noise - takes named arguments:x
= x inputy
= y inputseed0
= used to seed basis noise - must be constantseed1
= used to seed a different part of basis noise - must be constantinput_scale
= x/y inputs will be multiplied by this - must be constant - defaults to 1output_scale
= output will be multiplied by this - must be constant - defaults to 1
factorio-multioctave-noise
( ... ) - accumulate factorio basis noise across multiple scales - takes named arguments:x
= x inputy
= y inputseed0
= used to seed basis noise - must be constantseed1
= used to seed a different part of basis noise - must be constantinput_scale
= x/y inputs will be multiplied by this - must be constant - defaults to 1output_scale
= output will be multiplied by this - must be constant - defaults to 1octave_count
= number of different scales ('octaves', to somewhat abuse the term) to sampleoctave_input_scale_multiplier
- for each subsequent octave, input scale will be multiplied by thisoctave_output_scale_multiplier
- for each subsequent octave, output scale will be multiplied by thisoctave_seed0_shift
- for each subsequent octave, this will be added to seed0
factorio-multioctave-noise
is less flexible,
but may be slightly more efficient than combining multiple factorio-basis-noise
s in the expression tree
due to reduced register use.
By default, noise programs named "default-(noise layer name)" (e.g. "default-elevation") are used to generate each noise layer. A different noise program (e.g. provided by a mod) can be specified in a map generation settings JSON file:
{
"terrain_segmentation": "normal",
"water": "normal",
"width": 0,
"height": 0,
...yaddah yaddah yaddah, autoplace_controls and some other things...
"_comment": "Let's override elevation:",
"property_expression_names":
{
"elevation": "terraced-basis-noise"
}
}
And then a save file created by doing
factorio --create saves/game-with-terraced-basis-noise.zip --map-gen-settings terraced-basis-noise.map-gen-settings.json
For this to work, there must be a mod loaded that provides a noise expression named "terraced-basis-noise"; something like:
-- assume some new_basis_noise has been defined that returns
-- a basis noise expression with automatically chosen seeds
data:extend
{
{
type = "noise-expression",
name = "terraced-basis-noise",
expression = noise.define_noise_function( function(x,y,tile,map)
local bn = new_basis_noise(x,y,32/map.segmentation)
local terrace_strength = new_basis_noise(x,y,32/map.segmentation)
return noise.terrace_for_cliffs(bn, terrace_strength, map)
end)
},
}