Edit Targets and Layer Muting#

So far, when you have authored to a stage, your edits have quietly gone to one place: the root layer. In a real pipeline that is rarely what you want. A modeler’s changes belong in the modeling layer, a shot override belongs in the shot layer, and a throwaway experiment belongs somewhere that never reaches disk.

This lesson covers the two controls that make that possible. The edit target decides where your edits are written. Layer muting decides which layers are read. Together they let you steer authoring and isolate contributions without restructuring the layer stack.

What Is an Edit Target?#

An edit target tells a stage which layer should receive authored opinions. Every stage always has one. Unless you change it, it is the root layer, which is why Usd.Stage.CreateNew() followed by authoring puts everything in that single file.

Changing the edit target does not change what the stage shows you. You still see the fully composed result. It only changes which layer your next edit is written into, and therefore how strong that edit is once composition resolves it.

Important

An edit target does not override strength ordering. Authoring into a weak layer writes a weak opinion, so a stronger layer can still win. If an edit appears to do nothing, check where it landed before assuming the API failed.

What Is Layer Muting?#

Muting a layer removes its contributions from composition without deleting the layer or editing the layer stack that refers to it. The layer keeps its data; the stage simply stops reading it.

The critical property is that muting is stage-level state, not scene description. It is not recorded in any layer, it is not saved, and another stage opened on the same root layer will not see it. That makes muting safe for exactly the things you would not want to persist: bisecting which layer introduced a bad opinion, checking what a scene looks like without the lighting department’s contributions, or skipping an expensive layer while iterating.

Working With Python#

The recommended way to change the edit target is UsdEditContext, a context manager that restores the previous target when the block exits:

1from pxr import Usd
2
3# Author into a specific layer, then go back to whatever was set before
4with Usd.EditContext(stage, some_layer):
5    prim.GetAttribute("radius").Set(5.0)

You can also set it directly with stage.SetEditTarget(layer) and read it back with stage.GetEditTarget(), but then restoring the previous target is your responsibility. Prefer the context manager.

Muting is driven from the stage using layer identifiers:

1stage.MuteLayer(layer.identifier)
2stage.UnmuteLayer(layer.identifier)
3stage.IsLayerMuted(layer.identifier)
4stage.GetMutedLayers()
5
6# Batch several changes into a single recomposition
7stage.MuteAndUnmuteLayers([mute_layer.identifier], [unmute_layer.identifier])

Each mute or unmute triggers a recomposition, so use UsdStage::MuteAndUnmuteLayers() when changing several layers at once rather than calling MuteLayer in a loop.

Caution

Muting matches on the layer identifier string, and it fails silently. If the identifier you pass does not match the one the stage resolved for that layer, GetMutedLayers will happily report your string while composition is completely unaffected.

This bites most often with relative paths: a layer opened as ./shot.usda and the same file opened as an absolute path are two different identifier strings. When muting does nothing, compare your identifier against stage.GetLayerStack() before looking anywhere else.

Examples#

Tip

You can run these examples locally as Jupyter notebooks. See How to Run Notebooks Locally for setup instructions.

Example 1: Directing Edits Into a Specific Layer#

We build a small layer stack with two sublayers: base.usda holding the published value, and shot.usda holding a stronger shot-level override. Notice that the root layer ends up empty, because every edit was directed somewhere else.

 1from pxr import Usd, UsdGeom, Sdf
 2
 3base = Sdf.Layer.CreateNew(asset_path("base.usda"))
 4shot = Sdf.Layer.CreateNew(asset_path("shot.usda"))
 5root = Sdf.Layer.CreateNew(asset_path("root.usda"))
 6
 7# Sublayers are listed strongest first
 8root.subLayerPaths = ["./shot.usda", "./base.usda"]
 9stage: Usd.Stage = Usd.Stage.Open(root)
10
11# The stage starts out targeting its root layer
12print("Edit target is the root layer:", stage.GetEditTarget().GetLayer() == root)
13
14# Author the published value into the weaker base layer
15with Usd.EditContext(stage, base):
16    ball: UsdGeom.Sphere = UsdGeom.Sphere.Define(stage, "/World/Ball")
17    ball.GetRadiusAttr().Set(1.0)
18
19# Author a shot-level override into the stronger shot layer
20with Usd.EditContext(stage, shot):
21    ball.GetRadiusAttr().Set(5.0)
22
23print("Composed radius:", ball.GetRadiusAttr().Get())
24print("base.usda holds:", base.GetAttributeAtPath("/World/Ball.radius").default)
25print("shot.usda holds:", shot.GetAttributeAtPath("/World/Ball.radius").default)
26print("root.usda is empty:", root.GetPrimAtPath("/World") is None)
27
28root.Save(); base.Save(); shot.Save()
Edit target is the root layer: True
Composed radius: 5.0
base.usda holds: 1.0
shot.usda holds: 5.0
root.usda is empty: True
True

The composed radius is 5.0 because shot.usda is the stronger sublayer. Both opinions still exist, each in the layer we targeted. Looking at all three layers makes the split concrete.

root.usda holds nothing but the sublayer list, even though it was the stage’s original edit target. Every edit was redirected elsewhere:

        
    

base.usda holds the full prim definition, because UsdGeom.Sphere.Define() ran while this layer was targeted:

        
    

shot.usda holds an over rather than a def. The prim already existed from base.usda, so targeting shot.usda authored an override of the radius instead of a second definition. The edit target decides which layer receives an opinion; OpenUSD still decides what kind of spec is needed to express it:

        
    

Example 2: Muting a Layer to Isolate a Contribution#

Now we mute the shot layer. The stage falls back to the base value, but nothing is edited or lost.

 1import os
 2
 3from pxr import Usd, UsdGeom, Sdf
 4
 5root = Sdf.Layer.FindOrOpen(asset_path("root.usda"))
 6shot = Sdf.Layer.FindOrOpen(asset_path("shot.usda"))
 7stage: Usd.Stage = Usd.Stage.Open(root)
 8ball: UsdGeom.Sphere = UsdGeom.Sphere.Get(stage, "/World/Ball")
 9
10print("Before muting:", ball.GetRadiusAttr().Get())
11
12stage.MuteLayer(shot.identifier)
13
14print("After muting: ", ball.GetRadiusAttr().Get())
15print("Muted layers: ", [os.path.basename(p) for p in stage.GetMutedLayers()])
16print()
17print("The layer still holds its opinion:", shot.GetAttributeAtPath("/World/Ball.radius").default)
18print("Muting written into the root layer?", "mute" in root.ExportToString().lower())
19
20stage.UnmuteLayer(shot.identifier)
21print("After unmuting:", ball.GetRadiusAttr().Get())
Before muting: 5.0
After muting:  1.0
Muted layers:  ['shot.usda']

The layer still holds its opinion: 5.0
Muting written into the root layer? False
After unmuting: 5.0

This is the bisecting workflow in miniature: mute a layer, see whether the problem disappears, and you have found which layer is responsible. Because muting is not scene description, you can do this on production files without any risk of committing the change.

Example 3: Muting Is Per-Stage#

To show that muting really is stage-local, we mute a layer on one stage and then open a second stage on the same root layer.

 1from pxr import Usd, UsdGeom, Sdf
 2
 3root = Sdf.Layer.FindOrOpen(asset_path("root.usda"))
 4shot = Sdf.Layer.FindOrOpen(asset_path("shot.usda"))
 5
 6stage_a: Usd.Stage = Usd.Stage.Open(root)
 7stage_a.MuteLayer(shot.identifier)
 8
 9# A completely separate stage opened on the same root layer
10stage_b: Usd.Stage = Usd.Stage.Open(root)
11
12print("stage_a sees the layer muted:", stage_a.IsLayerMuted(shot.identifier))
13print("stage_b sees the layer muted:", stage_b.IsLayerMuted(shot.identifier))
14print()
15print("stage_a radius:", UsdGeom.Sphere.Get(stage_a, "/World/Ball").GetRadiusAttr().Get())
16print("stage_b radius:", UsdGeom.Sphere.Get(stage_b, "/World/Ball").GetRadiusAttr().Get())
17
18stage_a.UnmuteLayer(shot.identifier)
stage_a sees the layer muted: True
stage_b sees the layer muted: False

stage_a radius: 1.0
stage_b radius: 5.0

Two stages built from identical scene description disagree about what the composed radius is, purely because one of them has a layer muted. That is the clearest demonstration that muting lives on the stage rather than in the data.

See also

The session layer is the other half of this picture. It is a scratch layer at the top of the root layer stack that makes a natural edit target for temporary, application-level overrides, and Usd.Stage.Save() deliberately does not write it to disk.

Key Takeaways#

Edit targets control where authored opinions are written, and layer muting controls which layers are read. Neither changes the structure of your layer stack, which is what makes both safe to use on production scenes.

Use UsdEditContext rather than SetEditTarget so the previous target is always restored, and remember that targeting a weak layer still produces a weak opinion. Use muting for temporary, exploratory work: it is stage-level state that is never saved, never shared between stages, and never recorded in scene description. When toggling several layers, batch the changes through MuteAndUnmuteLayers so composition is only rebuilt once.