UI Toolkit panels use a fixed GPU vertex layout that contains position, color, the primary UV channel, and internal channels for clipping, transforms, and other renderer state. Custom shaders authored in UI Shader Graph can only read additional inputs if the panel allocates GPU storage for them.
Use ExtraVertexChannels to append any combination of TexCoord1, TexCoord2, TexCoord3, Normal, and Tangent to each vertex. Custom mesh generation code writes values into those channels, and a custom shader graph reads them at the matching semantics: TEXCOORD1 to TEXCOORD3, NORMAL, and TANGENT.
Enable extra channels when a shader needs per-vertex data that the built-in layout doesn’t carry, such as normals for simulated lighting, tangent-space deformations, or per-vertex parameters for procedural effects.
To enable channels in code, set PanelSettings.extraVertexChannels for runtime panels, or IPanel.extraVertexChannels for Editor panels.
In the Inspector window of the Panel Settings asset, under Buffer Management, set Extra Vertex Channels to the channels you need. For more information, refer to Panel Settings properties reference.
The following example shows how to set the property in code:
// Enables extra channels on a runtime panel through its PanelSettings asset.
// Note: Changing extraVertexChannels at runtime is expensive because it regenerates all vertex
// buffers. Assign it statically in the PanelSettings asset in the Inspector instead.
public static void EnableChannelsOnRuntimePanel(PanelSettings panelSettings)
{
panelSettings.extraVertexChannels = ExtraVertexChannels.Normal | ExtraVertexChannels.TexCoord1;
}
// Enables extra channels on an Editor panel through the IPanel interface.
public static void EnableChannelsOnEditorPanel(IPanel panel)
{
panel.extraVertexChannels = ExtraVertexChannels.Normal;
}
If you change the channel set after the panel starts rendering, Unity rebuilds the panel’s render chain and preserves the runtime panel and its visual tree.
Each enabled channel adds 16 bytes per vertex on the GPU. Leave the property at None unless a shader on the panel needs the data.
Custom mesh generation runs inside a VisualElement.generateVisualContent callback. From there, allocate a UIMesh and submit it with MeshGenerationContext.DrawMesh.
You can populate a UIMesh with the temporary allocator, or with your own arrays.
Use MeshGenerationContext.AllocateTempMesh to allocate the slices. It returns a UIMesh whose mandatory vertices and indices slices are sized as requested, plus one slice per channel in the mask you provide. Channels you don’t request are empty default slices.
The following example shows how to allocate a temporary mesh with TexCoord1 and Normal channels, fill the slices, and submit the mesh:
// Allocates a quad and its extra channels with the temporary allocator, then submits it.
private static void DrawWithTempMesh(MeshGenerationContext mgc)
{
const int vertexCount = 4;
const int indexCount = 6;
mgc.AllocateTempMesh(
ExtraVertexChannels.Normal | ExtraVertexChannels.TexCoord1,
vertexCount, indexCount,
out UIMesh mesh);
NativeSlice<Vertex> vertices = mesh.vertices;
NativeSlice<ushort> indices = mesh.indices;
NativeSlice<Vector3> normals = mesh.normal;
NativeSlice<Vector4> uv1 = mesh.uv1;
// Corners of the element's content rectangle, in clockwise winding order.
Rect rect = mgc.visualElement.contentRect;
var corners = new Vector2[]
{
new Vector2(rect.xMin, rect.yMin),
new Vector2(rect.xMax, rect.yMin),
new Vector2(rect.xMax, rect.yMax),
new Vector2(rect.xMin, rect.yMax)
};
for (int i = 0; i < vertexCount; ++i)
{
vertices[i] = new Vertex
{
position = new Vector3(corners[i].x, corners[i].y, Vertex.nearZ),
tint = Color.white
};
normals[i] = new Vector3(0, 0, 1);
uv1[i] = new Vector4(i, 0, 0, 0);
}
indices[0] = 0;
indices[1] = 1;
indices[2] = 2;
indices[3] = 0;
indices[4] = 2;
indices[5] = 3;
mgc.DrawMesh(ref mesh);
}
Use this method in most cases. The slices stay valid until the renderer flushes the current repaint pass, which is always after generateVisualContent returns.
If you already have channel data in your own NativeArray<T>, for example from an upstream Burst job, call Slice on each array, assign the result to the matching UIMesh field, and submit the mesh.
The following example shows how to provide your own arrays for vertices, indices, and the Normal channel:
// Submits a mesh whose channel data comes from buffers that this component owns.
private void DrawWithOwnArrays(MeshGenerationContext mgc)
{
// This component owns these slices and keeps them valid until the next
// MeshGenerationContext flush, at the end of the current repaint pass.
var mesh = new UIMesh
{
vertices = m_VertexBuffer.Slice(),
indices = m_IndexBuffer.Slice(),
normal = m_NormalBuffer.Slice(),
};
mgc.DrawMesh(ref mesh);
}
The slices inside UIMesh are read at flush time, not when DrawMesh returns.
Important: All non-empty slices in the UIMesh must remain valid until the next MeshGenerationContext flush, which is the end of the current repaint pass. Slices returned by AllocateTempMesh are valid for that whole window.
DrawMesh enforces two rules when you submit a mesh. Unity logs violations with Debug.LogError and drops the offending data. The renderer doesn’t throw exceptions from per-frame mesh generation code.
Every non-empty extra channel slice must have the same length as vertices. If any lengths disagree, Unity drops the entire draw, because indices might reference vertex slots that don’t exist in the shorter slice.
If you provide a slice for a channel that the panel didn’t enable through extraVertexChannels, Unity clears that slice from the draw and processes the rest of the draw. The panel has no GPU slot for that channel, so a shader written against it reads undefined data.
You can draw without any extra channels on a panel that has them enabled, and you can provide a subset of the enabled channels.
If a draw provides at least one extra channel, Unity zero-fills the enabled channels that you leave empty. If a draw provides no extra channels, Unity leaves those vertex slots untouched, so use this only when the shader bound to those vertices doesn’t read them.
In a UI Shader Graph, add one of the following nodes to read an extra channel:
These nodes connect automatically to the matching channels in VertexDescriptionInputs and the matching semantics in the generated vertex shader. The Shader Graph editor accepts UV0 to UV3 on UI materials. UV4 and UV5 are reserved for internal renderer use.
The Bitangent Vector node derives its value from the normal and the tangent, so enable both the Normal and Tangent channels on the panel for it to produce meaningful values.
Set the Space property on the Normal Vector, Tangent Vector, and Bitangent Vector nodes to Object. UI Toolkit doesn’t apply a per-element world transform during rendering, so the other space options multiply the value by an unrelated matrix and produce incorrect data. Object returns the value exactly as the mesh generation code packed it into the vertex stream.
Important: The Normal Vector, Tangent Vector, and Bitangent Vector nodes work only in the vertex stage of a UI shader graph. To read these values in the fragment stage, connect the node to a Custom Interpolator block in the vertex context, then read the matching Custom Interpolator node in the fragment context.
A shader graph asset doesn’t specify which panel uses it. Set extraVertexChannels on every panel that hosts a VisualElement whose mesh data feeds into your shader.