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.
HasPayloadreports 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 itfalse.
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
- Assign an asset with Create & Assign.
- Click Refresh Registerer.
- 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
trueeven 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
falsesilently 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
.assetfile 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.
SourceTerrainDatais get-only and editor-only. UseRebindto change it; it's stripped from player builds so the asset never drags aTerrainDatainto them.- An unsaved asset referenced by a scene component is serialized into that scene and ships with it — no
.assetfile 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.