Table of Contents

BRG Extracted Terrain Data


An optional ScriptableObject holding terrain data that the renderer would otherwise read back from TerrainData when a terrain initializes — detail layers, trees already sorted into spatial chunks, holes, and the heightmap. Reading it is a memcpy instead of a set of allocating Unity getters.

Assign one to the Pre-Baked Terrain Data field on the BRG Terrain Registerer. When it's present and matches the terrain, it's used instead of the TerrainData readback path.

Assigning the asset is the whole setup. The payload is filled automatically during a player build, and in the editor a non-serialized cache fills itself on first use. You don't have to bake anything by hand.

Note

This is an optimization, not a requirement. Without an assigned asset the registerer reads TerrainData as normal — leaving the field empty is how a terrain opts out.

Two Separate Stores

The asset holds its data in two places, and the difference is the whole point of the design.

Editor cache Serialized payload
Stored where In memory on the asset instance On disk, in the .asset file
Serialized No Yes
Filled by Automatically, when the registerer initializes The build step, into the build's scene copy
Survives domain reload No Yes
Reaches version control Never Not from a build — only if you bake by hand
Ships in a player build No Yes

The extracted data is multi-megabyte and duplicates what's already in the TerrainData. Committing it would bloat the repository with a redundant copy that goes stale the moment someone paints the terrain — avoiding that is why the editor cache exists and is deliberately not serialized.

The automatic build step keeps that property: it bakes into the build's temporary copy of the scene, so the payload ships while the project asset on disk stays empty.

When both stores hold data, the editor cache wins.

HasPayload reports only on the serialized payload. It answers "does this asset carry data that ships in a build", not "does this asset have data available right now" — a warm editor cache alone leaves it false.

Setup

On the BRG Terrain Registerer inspector, under Extracted Data:

Control When it appears Description
Pre-Baked Terrain Data Always The asset reference. Assigning one is the opt-in; leaving it empty means this terrain always reads TerrainData and is skipped by the build step.
Create & Assign No asset assigned Creates a new asset next to the terrain's TerrainData (named <TerrainData>_BRGExtracted.asset) and assigns it. Disabled if the terrain has no TerrainData.
Point At This Terrain Assigned asset targets a different terrain Rebinds the asset to this terrain and clears whatever it had cached, since that data described the old terrain.
Refresh Registerer Asset assigned and matching Re-runs registration. The registerer is the only thing that reads TerrainData, so a refresh is what warms the editor cache.
Clear Editor Cache Asset assigned and matching Drops the in-memory cache. Disabled when the cache is already empty.

If the assigned asset points at a different TerrainData than the terrain it's on, the inspector shows a warning naming both — any payload it carries describes the wrong terrain and will be rejected at load.

Warming the cache

  1. Assign an asset with Create & Assign.
  2. Click Refresh Registerer.
  3. Payloads > Editor cache now reports a size and section count.

Subsequent refreshes read the cache instead of TerrainData.

This only affects editor iteration — for builds, assigning the asset in step 1 is all that's required, since the build step does its own extract.

Reading the Asset Inspector

Selecting the asset itself shows:

Row Meaning
Source Terrain Data The TerrainData this asset was extracted from. Read-only — use Rebind to point it at a different terrain. Editor-only, and dropped from player builds so the asset never pulls TerrainData into them as a dependency.
Payloads > Serialized Size of the on-disk payload, or "none — only a build step writes this". "none" is normal — the automatic build step bakes into the built scene, not the project asset. A size here means someone baked manually.
Payloads > Editor cache Size and section count of the in-memory cache, or "none — fills on the next terrain refresh".
Payloads > Runtime would use Which store would actually be read right now: editor cache, serialized payload, or "neither — reads TerrainData".

Sections

Each row reports whether that piece of data is present and how big it is.

Section Reads as
Detail layers <res> x <res>, <n> of <total> painted, <size> — resolution, how many detail layers actually have paint (unpainted layers aren't stored), and section size.
Trees <n> instances in <n> cells (<size>m), <size> — instance count, spatial cell count, the tree chunk size they were sorted with, and section size.
Holes <res> x <res>, <size>
Heightmap <res> x <res>, <size>

A section reads absent when that data hasn't been extracted, or when the section is too short to be valid.

Check this list after a bake. BakeFor returns true even when a section was skipped, so the Sections list is how you confirm what actually landed.

If the asset carries a saved payload, an info box appears with a Clear Saved Payload button. A saved payload is the expected result of a manual bake — the note is a reminder to keep it out of version control, since it duplicates data already in the TerrainData. The automatic build step never produces one on the project asset.

Baking During a Player Build

Player builds bake automatically. The step is controlled by Auto Bake Pre-Extracted Terrain Data On Build on the Terrain Config, which is on by default.

For each scene in the build it walks every TerrainBRGRegisterer — including inactive ones — and bakes any that have an asset assigned. When a terrain is baked it logs a summary naming the scene and the payload size per terrain:

BRGExtractedTerrainData: baked 2 terrain(s) into 'Level_01'
  Terrain_North: 4.21 MB
  Terrain_South: 3.87 MB

Opting a terrain out

Leave Pre-Baked Terrain Data empty on that registerer. The build step skips registerers with no asset assigned, and the terrain reads TerrainData as normal.

To disable baking project-wide, turn off Auto Bake Pre-Extracted Terrain Data On Build in the config.

The step does nothing when entering play mode — only actual player builds bake.

Scripting API

TerrainBRGRegisterer.ExtractedTerrainData

public BRGExtractedTerrainData ExtractedTerrainData { get; set; }

The asset bound to a registerer. Assigning while the registerer is already registered triggers a Refresh() so it rebuilds from the new asset — which means a procedural setup can bind an asset before the registerer is enabled and pay nothing for the swap.

BRGExtractedTerrainDataBaker

Editor-only static helpers in the BRGInstancedRenderer.Editor namespace.

Method Description
CreateUnsaved(TerrainData terrainData = null) Creates an unsaved instance. For procedurally built terrain whose TerrainData has no asset path — the caller decides whether it lives in a scene or gets written somewhere.
CreateFor(TerrainData terrainData) Creates the asset on disk, next to the TerrainData, so the pair stays together when either is moved. Returns null and logs an error if the TerrainData isn't a saved asset.
Rebind(BRGExtractedTerrainData asset, TerrainData terrainData) Points an asset at a different terrain and clears both stores, since anything cached describes the old one. This is the only way to change SourceTerrainData, which is get-only.
BakeFor(TerrainBRGRegisterer registerer) The whole bake for one terrain. See BakeFor below.
BakeForBuild(TerrainBRGRegisterer registerer) What the automatic build step calls. Clones project assets first so they stay empty. See BakeForBuild below.
BakeToAsset(BRGExtractedTerrainData asset) Promotes the warm editor cache to the serialized payload. Returns false and logs an error if the cache is empty.
Clear(BRGExtractedTerrainData asset) Clears both the serialized payload and the editor cache.

CreateUnsaved vs CreateFor

Use CreateFor for terrain authored in the editor — the TerrainData is a saved asset, so the extracted asset can be placed beside it.

Use CreateUnsaved for procedurally generated terrain, where the TerrainData exists only in memory and has no asset path. CreateFor fails in that case.

An unsaved instance referenced by a component in a scene is serialized into that scene and ships with it — which is what makes CreateUnsaved viable for procedural terrain with no TerrainData asset on disk.

BakeFor

public static bool BakeFor(TerrainBRGRegisterer registerer)

Creates and binds an asset if the registerer has none, forces a fresh extract, promotes the result to the serialized payload, and marks the owning scene dirty.

  • Edit mode only. The registerer must be active and enabled — otherwise it can't read its terrain, so the call logs an error and returns false.
  • Always re-extracts. Both stores are cleared before the refresh, so a stale payload can't refill the cache from itself. Re-baking a regenerated terrain is safe.
  • Returns true even if a section was skipped. A terrain with more than 64 tree prototypes or more than 64 detail layers bakes without that section. Check the asset inspector's Sections list to confirm what actually landed.
  • Marks the scene dirty but does not save it. See below.
  • Works in -batchmode -nographics, so it can run on a CI agent.

BakeForBuild

public static bool BakeForBuild(TerrainBRGRegisterer registerer)

The entry point the automatic build step uses. Differs from BakeFor in two ways:

  • Returns false silently when the registerer has no asset assigned — that's the opt-out, not an error.
  • Never dirties a project asset. If the assigned asset lives on disk, it's cloned into an unsaved instance first and the registerer is pointed at the clone. The bake lands in the clone, which is serialized into the scene being built, so the .asset file in your project stays empty.

Otherwise it delegates to BakeFor, so all of that method's behavior applies.

Manual baking

Calling BakeFor yourself marks the scene dirty but does not save it. Inside IProcessSceneWithReport that's fine, since the build serializes live memory. Anywhere else — a menu item, an IPreprocessBuildWithReport, a loop over open scenes — you must save the scene or the bake is silently lost:

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using BRGInstancedRenderer;
using BRGInstancedRenderer.Editor;

public static class BakeTerrainMenu
{
    [MenuItem("Tools/Bake BRG Terrain Data")]
    public static void BakeOpenScene()
    {
        foreach (var reg in Object.FindObjectsByType<TerrainBRGRegisterer>(
                     FindObjectsSortMode.None))
        {
            if (BRGExtractedTerrainDataBaker.BakeFor(reg))
                Debug.Log($"Baked {reg.name}", reg);
        }

        // BakeFor only marks the scene dirty - without this the bake is lost
        EditorSceneManager.SaveOpenScenes();
    }
}
Important

Unlike the automatic build step, a manual BakeFor on a project asset writes a multi-megabyte payload to disk. Revert it afterwards if you don't want it committed — BRGExtractedTerrainDataBaker.Clear(asset) clears both stores.

Keeping It Fresh

The editor cache is a snapshot. Once the terrain changes underneath it, it's stale.

#if UNITY_EDITOR
asset.InvalidateEditorCache();
#endif

Call this whenever the terrain's trees or detail data are repainted or edited, then refresh the registerer to re-warm it. Clear Editor Cache in either inspector does the same thing.

The cache doesn't survive a domain reload, so a script recompile or play-mode entry clears it on its own.

Note

InvalidateEditorCache() and SourceTerrainData are both compiled out of player builds. Guard any call site in runtime code with #if UNITY_EDITOR. HasPayload is available at runtime.

Verifying It Works

Open the Profiler and look at terrain initialization:

Marker Meaning
DetailRegisterer.LoadFromScriptableObject Detail data came from the asset.
TreeRegisterer.LoadFromScriptableObject Tree data came from the asset.
DetailRegisterer.CacheTerrainData Detail data was read from TerrainData — the asset was missing, empty, or rejected.
DetailRegisterer.CacheToScriptableObject / TreeRegisterer.CacheToScriptableObject TerrainData was read and mirrored into the editor cache, so the next refresh can use it.

Seeing a LoadFromScriptableObject marker is the confirmation that the fast path ran.

Measured on one terrain on one machine, so treat any figure you get as a data point rather than a guarantee — the win depends on terrain resolution, detail layer count, and tree count.

Fallback Behaviour

A payload that doesn't match the terrain is rejected, and the normal TerrainData path runs. Nothing breaks, and nothing renders differently — you just don't get the speedup.

Detail data is rejected when:

  • The section is missing or shorter than its header.
  • The baked detail resolution doesn't match the terrain's. (logs a warning naming both)
  • The baked detail layer count doesn't match the terrain's. (logs a warning naming both)
  • The painted layer blocks don't all fit inside the section.

The heightmap and holes sections are rejected when the section is missing, too short for the grid it claims, or its baked resolution doesn't match.

Tree data is rejected when:

  • The section is missing or shorter than its header. (silent)
  • The instance, index, and cell regions don't all fit inside the section. (silent)
  • The baked terrain size, tree chunk size, or prototype count changed. (logs a warning naming the reason)
  • A prototype the payload has instances for failed to register — dropping its instances would shift every baked cell range. (logs a warning naming the prototype)

A moved terrain still loads from the payload; a resized or re-gridded one can't.

What logs and what doesn't

Section On rejection
Detail layers Warning, when the resolution or layer count mismatches. Silent otherwise.
Trees Warning, naming the reason — terrain size, chunk size, prototype count, or the prototype that failed to register. Silent for malformed-section cases.
Heightmap Always silent.
Holes Always silent.

If the heightmap or holes section is quietly falling back, nothing in the console will tell you — check the Profiler markers above instead.

Notes

  • One asset per terrain. Sharing an asset between registerers on different terrains means at most one of them matches — the rest are rejected at load.
  • The automatic build step bakes into the built scene, never the project asset — so an empty Serialized row in the inspector is the normal, expected state for a project asset.
  • SourceTerrainData is get-only and editor-only. Use Rebind to change it; it's stripped from player builds so the asset never drags a TerrainData into them.
  • An unsaved asset referenced by a scene component is serialized into that scene and ships with it — no .asset file required.
  • Baking works in -batchmode -nographics, so it can run on a CI agent.
  • The payload byte format is internal and may change between versions. Re-bake after upgrading rather than shipping an old payload.