MultiLoader 1.21+ · Part 28

Custom Biomes (MultiLoader 1.21+)

ADVANCED MULTILOADER 1.21-1.21.1 28 min read · Nov 16, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

Custom biomes in 1.21.1 are fully data-driven: the biome definition is a JSON file in data/<namespace>/worldgen/biome/, and injecting it into the overworld requires knowing the climate parameter space the world generator uses to pick biomes. This tutorial covers creating the biome JSON, generating it with datagen, and injecting it into the overworld using TerraBlender, the standard multiloader library for custom biome placement.

NOTE
Complete the Ore Generation tutorial before continuing. It introduces DatapackBuiltinEntriesProvider, which is used again here for biome datagen.

The Biome JSON

Create the file src/main/resources/data/examplemod/worldgen/biome/example_biome.json in your common project. The full structure of a biome JSON:

json
{
"temperature": 0.7,
"downfall": 0.5,
"has_precipitation": true,
"temperature_modifier": "none",
"effects": {
"sky_color": 7972607,
"fog_color": 12638463,
"water_color": 4159204,
"water_fog_color": 329011,
"grass_color_modifier": "none",
"mood_sound": {
"sound": "minecraft:ambient.cave",
"tick_delay": 6000,
"block_search_extent": 8,
"offset": 2.0
}
},
"spawners": {
"monster": [
{
"type": "minecraft:zombie",
"weight": 100,
"minCount": 4,
"maxCount": 4
}
],
"creature": [],
"ambient": [],
"water_creature": [],
"underground_water_creature": [],
"water_ambient": [],
"misc": [],
"axolotls": []
},
"spawn_costs": {},
"carvers": {
"air": ["minecraft:cave", "minecraft:canyon"]
},
"features": [
[],
[],
[],
[],
[],
["minecraft:spring_water", "minecraft:spring_lava"],
["minecraft:amethyst_geode"],
[
"minecraft:ore_dirt",
"minecraft:ore_gravel",
"minecraft:ore_granite_upper",
"minecraft:ore_granite_lower",
"minecraft:ore_diorite_upper",
"minecraft:ore_diorite_lower",
"minecraft:ore_andesite_upper",
"minecraft:ore_andesite_lower"
],
[],
[],
[]
]
}

The fields in detail:

  • temperature: Float, 0.0 to 2.0. Controls whether precipitation falls as rain (above 0.15), snow (below 0.15), or nothing (above 1.0 in the case of some desert variants). Also affects which farmland crops grow without irrigation.
  • downfall: Float, 0.0 to 1.0. Controls grass and foliage colour interpolation and the wetness of the biome.
  • has_precipitation: Boolean. Set to false for dry biomes like deserts and badlands.
  • temperature_modifier: "none" or "frozen". The frozen modifier is used by frozen ocean variants to make surface water freeze regardless of altitude.

Biome Effects

The effects block controls all visual and audio properties:

  • sky_color: Decimal integer encoding an RGB colour for the sky. Use an online hex-to-decimal converter; the vanilla forest sky is 7972607 (#79a8ff roughly).
  • fog_color: The distant fog colour. Most overworld biomes use 12638463.
  • water_color: The tint applied to water blocks. 4159204 is vanilla ocean blue.
  • water_fog_color: The underwater fog tint.
  • grass_color_modifier: "none", "dark_forest", or "swamp". Dark forest applies a multiplicative darkening; swamp shifts grass to a muted yellow-green.
  • foliage_color: Optional. When absent, foliage colour is derived from temperature and downfall. Provide an integer to override it entirely.
  • grass_color: Optional. Same as foliage_color but for grass blocks.
  • mood_sound: The ambient cave sound event, how many ticks between plays, the block search radius, and the spatial offset from the listener.
  • ambient_sound: Optional. A looping background sound (used by the Nether biomes).
  • additions_sound: Optional. A randomly played addition sound alongside the ambient sound.
  • music: Optional. Background music configuration (sound, min/max delay, whether to replace current music).
  • particle: Optional. A particle that appears randomly in the air, like nether ash or warped spores.

Mob Spawning

The spawners block lists mob spawn entries per mob category. Each entry requires a type (the entity registry name), a weight (relative frequency compared to others in the same category), and minCount and maxCount (the pack size range):

json
"spawners": {
"monster": [
{ "type": "minecraft:zombie", "weight": 100, "minCount": 4, "maxCount": 4 },
{ "type": "minecraft:skeleton", "weight": 80, "minCount": 4, "maxCount": 4 },
{ "type": "minecraft:spider", "weight": 100, "minCount": 4, "maxCount": 4 }
],
"creature": [
{ "type": "minecraft:sheep", "weight": 12, "minCount": 4, "maxCount": 4 },
{ "type": "minecraft:pig", "weight": 10, "minCount": 4, "maxCount": 4 }
],
"ambient": [
{ "type": "minecraft:bat", "weight": 10, "minCount": 8, "maxCount": 8 }
],
"water_creature": [],
"underground_water_creature": [
{ "type": "minecraft:glow_squid", "weight": 10, "minCount": 4, "maxCount": 6 }
],
"water_ambient": [],
"misc": [],
"axolotls": []
}

spawn_costs is the energy-based spawn limiting system for mobs that use it (wolves, foxes, and similar animals). Most modded mobs do not need an entry here unless they extend one of the vanilla entity types that uses it.

Registering the Biome Key

Create a BiomeKeys class in your common project to hold the ResourceKey for your biome. This key is used both in datagen and at runtime when referencing the biome from code:

java
public class BiomeKeys {
public static final ResourceKey<Biome> EXAMPLE_BIOME =
ResourceKey.create(
Registries.BIOME,
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "example_biome")
);
}

Datagen via DatapackBuiltinEntriesProvider

Instead of writing the JSON by hand, use the Java builder API in a bootstrap function. This is particularly useful for biomes because the feature list is long and error-prone to type manually. Create a bootstrap class:

java
public class ExampleBiomeBootstrap {
public static void bootstrap(BootstrapContext<Biome> context) {
HolderGetter<PlacedFeature> features = context.lookup(Registries.PLACED_FEATURE);
HolderGetter<ConfiguredWorldCarver<?>> carvers = context.lookup(Registries.CONFIGURED_CARVER);
context.register(BiomeKeys.EXAMPLE_BIOME, createExampleBiome(features, carvers));
}
public static Biome createExampleBiome(
HolderGetter<PlacedFeature> features,
HolderGetter<ConfiguredWorldCarver<?>> carvers) {
BiomeGenerationSettings.Builder gen = new BiomeGenerationSettings.Builder(features, carvers);
// Add vanilla cave carvers
gen.addCarver(GenerationStep.Carving.AIR, Carvers.CAVE);
gen.addCarver(GenerationStep.Carving.AIR, Carvers.CANYON);
// Add standard underground ore features
BiomeDefaultFeatures.addDefaultOres(gen);
BiomeDefaultFeatures.addDefaultSoftDisks(gen);
// Add surface features specific to your biome here:
// gen.addFeature(GenerationStep.Decoration.VEGETAL_DECORATION, VegetationPlacements.TREES_PLAINS);
MobSpawnSettings.Builder spawns = new MobSpawnSettings.Builder();
BiomeDefaultFeatures.farmAnimals(spawns);
spawns.addSpawn(MobCategory.MONSTER,
new MobSpawnSettings.SpawnerData(EntityType.ZOMBIE, 100, 4, 4));
return new Biome.BiomeBuilder()
.hasPrecipitation(true)
.temperature(0.7f)
.downfall(0.5f)
.specialEffects(new BiomeSpecialEffects.Builder()
.skyColor(7972607)
.fogColor(12638463)
.waterColor(4159204)
.waterFogColor(329011)
.ambientMoodSound(AmbientMoodSettings.LEGACY_CAVE_SETTINGS)
.build())
.mobSpawnSettings(spawns.build())
.generationSettings(gen.build())
.build();
}
}

Register it in your DatapackBuiltinEntriesProvider:

java
RegistrySetBuilder registryBuilder = new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, ExampleFeatures::bootstrapConfigured)
.add(Registries.PLACED_FEATURE, ExampleFeatures::bootstrapPlaced)
.add(Registries.BIOME, ExampleBiomeBootstrap::bootstrap);

Adding the Biome to the World

Creating the biome JSON makes the biome exist as a registry entry, but it does not cause it to generate anywhere in the world. World generation in 1.21.1 uses a noise-based climate parameter space to select biomes: every point in the world has values for temperature, humidity, continentalness, erosion, weirdness, and depth, and the biome source maps those values to a biome choice.

Injecting a new biome into the overworld requires either writing a custom BiomeSource (very complex, and the existing source cannot be easily replaced without affecting all other mods) or using TerraBlender, a library that provides a safe region-based API for both loaders. TerraBlender is the de facto standard for overworld biome injection in 1.21.1 and supports both Fabric and NeoForge from a single codebase.

NOTE
Adding biomes to the Nether is handled differently: the Nether biome source uses a separate climate space and can be modified via NeoForge's BiomeModifier system or Fabric's BiomeModifications.addProperties(). The TerraBlender approach described below targets the overworld only.

TerraBlender Setup

Add TerraBlender as a dependency in each loader subproject. Find the latest version numbers on Modrinth:

groovy
// Both subprojects need the TerraBlender maven repository
repositories {
maven { url = "https://maven.minecraftforge.net" }
// or wherever TerraBlender is hosted — check the TerraBlender README for the current repo URL
}
// neoforge/build.gradle
dependencies {
implementation "com.github.glitchfiend:TerraBlender-neoforge:1.21.1-VERSION"
}
// fabric/build.gradle
dependencies {
modImplementation "com.github.glitchfiend:TerraBlender-fabric:1.21.1-VERSION"
}

On NeoForge, add TerraBlender to your mods.toml dependencies block so it is listed as a required dependency. On Fabric, add it to fabric.mod.jsonunder depends.

TIP
TerraBlender ships a common API jar that you can optionally add as a compileOnly dependency to your common module. This gives you compile-time access to the shared Region and Regions classes without pulling in the full runtime on the common classpath.

Defining a Region

TerraBlender organises biome injection around regions. A region defines which biomes it contributes to the climate parameter space and how heavily it participates (the weight). A weight of 1 means the region occupies roughly 1/(1 + total vanilla weight) of the biome selection probability. Increase it to make your biome appear more frequently.

Create a ExampleBiomeRegion class in your common project:

java
public class ExampleBiomeRegion extends Region {
public ExampleBiomeRegion(int weight) {
super(
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "overworld"),
RegionType.OVERWORLD,
weight
);
}
@Override
public void addBiomes(Registry<Biome> biomeRegistry,
Pair<Climate.ParameterPoint, ResourceKey<Biome>> mapper) {
// Replace vanilla forest biome selections with our custom biome.
// This uses TerraBlender's helper to find all points where the forest would
// generate and substitutes our biome at those locations instead.
this.addModifiedVanillaBiomes(biomeRegistry, mapper, (point, key) -> {
if (key.equals(Biomes.FOREST)) {
return Optional.of(BiomeKeys.EXAMPLE_BIOME);
}
return Optional.empty();
});
}
}

Alternatively, you can manually specify the climate parameters where your biome should generate. Climate parameters range from -1.0 to 1.0 on each axis. Use Climate.Parameter.span(min, max) for ranges and Climate.Parameter.point(value) for exact values:

java
@Override
public void addBiomes(Registry<Biome> biomeRegistry,
Pair<Climate.ParameterPoint, ResourceKey<Biome>> mapper) {
this.addBiome(mapper,
Climate.parameters(
Climate.Parameter.span(0.0f, 0.55f), // temperature: warm
Climate.Parameter.span(0.0f, 0.5f), // humidity: moderate
Climate.Parameter.span(0.03f, 1.0f), // continentalness: inland
Climate.Parameter.span(-0.37f, 0.45f), // erosion: flat to moderate
Climate.Parameter.point(0.0f), // depth: surface (0.0 = surface, 1.0 = underground)
Climate.Parameter.span(-0.56f, 0.56f), // weirdness: any
0L // offset: 0 for normal priority
),
BiomeKeys.EXAMPLE_BIOME
);
}

Register the region from your common initialiser. Because TerraBlender is a required dependency on both loaders, this call works in common code with no loader-specific branching:

java
// In CommonClass.init():
Regions.register(new ExampleBiomeRegion(2));

The integer argument is the weight. A value of 2 gives your biome roughly twice as much representation as a region with weight 1. Start with 1-2 for a biome that appears occasionally, and increase it if the biome is too rare during testing.

TIP
TerraBlender's addModifiedVanillaBiomes helper replaces specific vanilla biome points rather than adding entirely new ones, which keeps the total climate parameter space balanced. Use the manual addBiome call if you want your biome to appear in areas where no vanilla biome would otherwise generate, such as very unusual climate combinations. Avoid overlapping climate ranges heavily with vanilla biomes, as this can make vanilla biomes disappear from the world.

Lang Entry

Add a translation entry for the biome name. This is displayed in the F3 debug screen and in any context that shows the current biome (such as the /locate biomecommand output):

json
"biome.examplemod.example_biome": "Example Biome"

Testing

Create a new world with both TerraBlender and your mod installed. Run /locate biome examplemod:example_biome in chat. Minecraft will search the surroundings and report the nearest coordinates of your biome. Teleport there to confirm it generates with the correct sky colour, fog, water colour, and mob spawns.

If the /locate biome command reports "biome not found", check:

  • That the JSON file path is correct: data/examplemod/worldgen/biome/example_biome.json.
  • That Regions.register() was called before the world is loaded.
  • That TerraBlender is installed and loading correctly (check the mod list on the title screen).
  • That you are searching in a newly generated world. Existing chunks in old worlds will not contain the new biome.

Use /tp @s ~ ~ ~ followed by pressing F3 to see the current biome name once you have teleported to the reported coordinates. The biome name should show as "Example Biome" in the debug overlay.

You can find the source for this tutorial here:

View Source on GitHub