Custom vertex data
阅读时间5 分钟最后更新于 1 个月前
UI Toolkit panels ship a fixed GPU vertex layout: position, color, the primary UV channel, and a handful of internal channels used for clipping, transforms, and other renderer state. Custom shaders authored in UI Shader Graph can't read additional inputs unless the panel allocates the storage for them on the GPU.
The opt-in lets a panel append any combination of , , , , and to each vertex. Custom mesh generation code can then push values into those channels, and a custom Shader Graph can read them at the same semantics (-, , ).
ExtraVertexChannelsTexCoord1TexCoord2TexCoord3NormalTangentTEXCOORD1TEXCOORD3NORMALTANGENTUse this when you need to feed data into a shader that the built-in vertex layout doesn't carry — for example, fake-lighting normals, tangent-space deformations, or extra per-vertex parameters for procedural effects.
Enable channels on a panel
The opt-in lives on for runtime panels, and on for Editor panels.
PanelSettings.extraVertexChannelsIPanel.extraVertexChannelsIn the Panel Settings Inspector, find the Extra Vertex Channels dropdown under Buffer Management and enable the flags you need:
// Runtime panel — at edit time on the PanelSettings asset, or at runtime in code.panelSettings.extraVertexChannels = ExtraVertexChannels.Normal | ExtraVertexChannels.TexCoord1;// Editor panel — set the flag on the IPanel directly.panel.extraVertexChannels = ExtraVertexChannels.Normal;
Changing the channel set after the panel has started rendering rebuilds the panel's render chain. The runtime panel and its visual tree are preserved.
Each enabled channel adds 16 bytes per vertex on the GPU. Leave the property at unless a shader on the panel actually needs the data.
NoneProvide channel data per draw
Custom mesh generation runs inside a callback. From there, allocate a and submit it through .
VisualElement.generateVisualContentUIMeshMeshGenerationContext.DrawMesh(ref UIMesh, Texture)There are two ways to populate a :
UIMeshAllocate from the temp allocator
The simplest path is to ask to allocate the slices for you. The allocator returns a whose mandatory / slices are sized as requested, plus one slice per channel in the mask you pass in. Channels you didn't request come back as empty default slices.
MeshGenerationContextUIMeshverticesindicesvoid OnGenerateContent(MeshGenerationContext mgc){ const int vertexCount = 4; const int indexCount = 6; mgc.AllocateTempMesh( ExtraVertexChannels.Normal | ExtraVertexChannels.TexCoord1, vertexCount, indexCount, out UIMesh mesh); for (int i = 0; i < vertexCount; ++i) { mesh.vertices[i] = new Vertex { position = ..., tint = Color.white }; mesh.normal[i] = new Vector3(0, 0, 1); mesh.uv1[i] = new Vector4(i, 0, 0, 0); } mesh.indices[0] = 0; mesh.indices[1] = 1; mesh.indices[2] = 2; mesh.indices[3] = 0; mesh.indices[4] = 2; mesh.indices[5] = 3; mgc.DrawMesh(ref mesh);}
This path is recommended for most cases. The slices are valid until the renderer flushes the current repaint pass, which is always after returns.
generateVisualContentBring your own arrays
If you already have channel data in your own (for example, produced by an upstream Burst job), assign each into the matching field and submit:
NativeArray<T>UIMeshvoid OnGenerateContent(MeshGenerationContext mgc){ // Slices owned by the caller. They must remain valid until the next // MeshGenerationContext flush (the end of the current repaint pass). NativeSlice<Vertex> vertices = m_VertexBuffer.AsSlice(); NativeSlice<ushort> indices = m_IndexBuffer.AsSlice(); NativeSlice<Vector3> normals = m_NormalBuffer.AsSlice(); var mesh = new UIMesh { vertices = vertices, indices = indices, normal = normals, }; mgc.DrawMesh(ref mesh);}
The slices inside are read at flush time, not when returns. All non-empty slices in the must remain valid until the next flush (the end of the current repaint pass). Slices returned by are guaranteed valid by the temp allocator's lifecycle.
UIMeshDrawMeshUIMeshMeshGenerationContextAllocateTempMeshValidation rules
DrawMesh(ref UIMesh, …)Debug.LogErrorLength consistency. Every non-empty extras slice must have the same length as . If any disagree, the entire draw is dropped — indices may reference vertex slots that don't exist in the shorter slice.
verticesChannel consistency. Providing a slice for a channel the panel did not enable via is an error. The offending slice is cleared from the draw and the rest of the draw proceeds. The panel has no GPU slot for that channel — a shader written against it would read undefined data anyway.
extraVertexChannelsIt's fine to draw without providing any extras on an extras-enabled panel, and it's fine to provide only a subset of the enabled channels. Within a draw that does provide extras, channels the panel enabled but the caller left empty are zero-filled in that draw's stream-1 vertex slots. Draws that provide no extras at all leave the stream-1 slots untouched — only meaningful if the shader bound for those vertices does not read stream 1.
Read the channels in a Shader Graph
In a UI Shader Graph, add a UV node (channel UV1, UV2, or UV3), Normal Vector node, Tangent Vector node, or Bitangent Vector node. They wire automatically to the matching channels in and the matching semantics in the generated vertex shader. The Shader Graph editor accepts UV0-UV3 on UI materials; UV4 and UV5 are reserved for internal renderer use. The Bitangent Vector node derives from the normal and tangent, so the panel must enable both the Normal and Tangent channels for it to produce meaningful values.
VertexDescriptionInputsSet the Space property on 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 would multiply the value by an unrelated matrix and produce incorrect data. Object returns the value exactly as it was packed into the vertex stream.
The Shader Graph asset doesn't know which panel will use it. It's your responsibility to set on every panel that hosts a whose mesh data feeds into your shader.
extraVertexChannelsVisualElement