Skip to content

Instantly share code, notes, and snippets.

@TOGoS
Created February 2, 2018 22:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TOGoS/97c0b905e1183e58ccf24fba6092bad6 to your computer and use it in GitHub Desktop.
Save TOGoS/97c0b905e1183e58ccf24fba6092bad6 to your computer and use it in GitHub Desktop.
Factorio programmable noise overview

Factorio Programmable Noise

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.

Programmable noise

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.

The Expression Tree

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 a variable_name referencing a predefined variable or constant
  • literal-number, which has a literal_value that is the expression's value
  • function-application, which has a function_name and array of arguments (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 range
  • factorio-basis-noise( ... ) - sample a layer of the factorio basis noise - takes named arguments:
    • x = x input
    • y = y input
    • seed0 = used to seed basis noise - must be constant
    • seed1 = used to seed a different part of basis noise - must be constant
    • input_scale = x/y inputs will be multiplied by this - must be constant - defaults to 1
    • output_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 input
    • y = y input
    • seed0 = used to seed basis noise - must be constant
    • seed1 = used to seed a different part of basis noise - must be constant
    • input_scale = x/y inputs will be multiplied by this - must be constant - defaults to 1
    • output_scale = output will be multiplied by this - must be constant - defaults to 1
    • octave_count = number of different scales ('octaves', to somewhat abuse the term) to sample
    • octave_input_scale_multiplier - for each subsequent octave, input scale will be multiplied by this
    • octave_output_scale_multiplier - for each subsequent octave, output scale will be multiplied by this
    • octave_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-noises in the expression tree due to reduced register use.

Overriding Noise Programs

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)
  },
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment