I am trying to create a gradient mask on an Image element like this:

is my .uss class i have this:
.compass__texture {
mask-image: linear-gradient(
to right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 1) 30%,
rgba(0, 0, 0, 1) 70%,
rgba(0, 0, 0, 0) 100%
);
}
Which end up looking like this:

after looking, this seems to not be supported…
What is my other option for achieving this effect? I can’t find any info on creating a “soft/gradient” mask within UI toolkit. I am willing to update versions if need be.
Is the content a simple texture ? If that’s the case, I think your best option might be use the Mesh API to generate a geometry where the alpha is encoded in each vertex.
You need as many segments as you want to create linear gradient steps, feel free to use this custom element as a starting point. You can give it any texture via C# or the Inspector.
If you want a more data driven solution you can have each of the step of the gradient encoded as parameters given to the element. For now your CSS declaration is approximated into a C# function (see CalculateGradientAlpha).
using UnityEngine;
using UnityEngine.UIElements;
[UxmlElement]
public partial class GradientMaskElement : VisualElement
{
// Number of segments to subdivide the gradient - more segments = smoother gradient
private const int GradientSegments = 5;
// Static arrays to hold vertices and indices for the mesh
readonly Vertex[] m_Vertices = new Vertex[(GradientSegments + 1) * 2];
readonly ushort[] m_Indices = new ushort[GradientSegments * 6];
[UxmlAttribute]
public Texture2D texture;
public GradientMaskElement()
{
// Initialize indices for triangle strips
for (int i = 0; i < GradientSegments; i++)
{
int baseIndex = i * 6;
int vertexIndex = i * 2;
// Two triangles per segment (quad)
// Triangle 1
m_Indices[baseIndex] = (ushort)(vertexIndex);
m_Indices[baseIndex + 1] = (ushort)(vertexIndex + 2);
m_Indices[baseIndex + 2] = (ushort)(vertexIndex + 3);
// Triangle 2
m_Indices[baseIndex + 3] = (ushort)(vertexIndex);
m_Indices[baseIndex + 4] = (ushort)(vertexIndex + 3);
m_Indices[baseIndex + 5] = (ushort)(vertexIndex + 1);
}
// Subscribe to the visual content generation
generateVisualContent += OnGenerateVisualContent;
}
private void OnGenerateVisualContent(MeshGenerationContext mgc)
{
Rect r = contentRect;
if (r.width < 0.01f || r.height < 0.01f)
return; // Skip rendering when too small
float width = r.width;
float height = r.height;
// Generate vertices for the gradient mesh
for (int i = 0; i <= GradientSegments; i++)
{
float t = (float)i / GradientSegments; // Normalized position (0 to 1)
float x = width * t;
float alpha = CalculateGradientAlpha(t);
// Set UV coordinates - using x position to subdivide texture horizontally
float u = t; // U coordinate varies from 0 to 1 across the width
// Top vertex
int topVertexIndex = i * 2;
m_Vertices[topVertexIndex].position = new Vector3(x, 0, Vertex.nearZ);
m_Vertices[topVertexIndex].tint = new Color(1, 1, 1, alpha); // White with calculated alpha
m_Vertices[topVertexIndex].uv = new Vector2(u, 1);
// Bottom vertex
int bottomVertexIndex = i * 2 + 1;
m_Vertices[bottomVertexIndex].position = new Vector3(x, height, Vertex.nearZ);
m_Vertices[bottomVertexIndex].tint = new Color(1, 1, 1, alpha); // White with calculated alpha
m_Vertices[bottomVertexIndex].uv = new Vector2(u, 0);
}
// Allocate mesh data and set vertices/indices
MeshWriteData mwd = mgc.Allocate(m_Vertices.Length, m_Indices.Length, texture);
mwd.SetAllVertices(m_Vertices);
mwd.SetAllIndices(m_Indices);
}
private float CalculateGradientAlpha(float t)
{
if (t <= 0.0f)
return 0.0f; // Fully transparent at start
else if (t <= 0.3f)
{
// Linear interpolation from 0 to 1 between 0% and 30%
return Mathf.Lerp(0.0f, 1.0f, t / 0.3f);
}
else if (t <= 0.7f)
{
// Fully opaque between 30% and 70%
return 1.0f;
}
else if (t <= 1.0f)
{
// Linear interpolation from 1 to 0 between 70% and 100%
return Mathf.Lerp(1.0f, 0.0f, (t - 0.7f) / 0.3f);
}
else
return 0.0f; // Fully transparent at end
}
}
mask-image is not supported. A filter could achieve this, but the most efficient way to render this is to encode the alpha in vertices and draw the texture with MeshGenerationContext (see GenerateVisualContent).
I should have included that this is a scrolling texture that is used for a compass. This touches on some aspects I have virtually no experience with, would you solution still allow for the texture to scroll left and right while maintaining that gradient mask?
You simply need to regenerate your mesh every frame to adjust the UVs. It shouldn’t be a problem.
This was my final script that supported scroll
using UnityEngine;
using UnityEngine.UIElements;
[UxmlElement]
public partial class GradientMask : VisualElement
{
[UxmlAttribute]
public Texture2D texture { get; set; }
// scroll offset property
private float _scrollOffset = 0;
// Number of segments to subdivide the gradient - more segments = smoother gradient
private const int GradientSegments = 5;
// Static arrays to hold vertices and indices for the mesh
readonly Vertex[] m_Vertices = new Vertex[(GradientSegments + 1) * 2];
readonly ushort[] m_Indices = new ushort[GradientSegments * 6];
public GradientMask()
{
// Initialize indices for triangle strips
for (int i = 0; i < GradientSegments; i++)
{
int baseIndex = i * 6;
int vertexIndex = i * 2;
// Two triangles per segment (quad)
// Triangle 1
m_Indices[baseIndex] = (ushort)(vertexIndex);
m_Indices[baseIndex + 1] = (ushort)(vertexIndex + 2);
m_Indices[baseIndex + 2] = (ushort)(vertexIndex + 3);
// Triangle 2
m_Indices[baseIndex + 3] = (ushort)(vertexIndex);
m_Indices[baseIndex + 4] = (ushort)(vertexIndex + 3);
m_Indices[baseIndex + 5] = (ushort)(vertexIndex + 1);
}
// Subscribe to the visual content generation
generateVisualContent += OnGenerateVisualContent;
}
// public method to update scroll
public void SetScrollOffset(float offset)
{
_scrollOffset = offset;
MarkDirtyRepaint(); // Request redraw when scroll changes
}
private void OnGenerateVisualContent(MeshGenerationContext mgc)
{
Rect r = contentRect;
if (r.width < 0.01f || r.height < 0.01f)
return; // Skip rendering when too small
float width = r.width;
float height = r.height;
// Generate vertices for the gradient mesh
for (int i = 0; i <= GradientSegments; i++)
{
float t = (float)i / GradientSegments;
float x = width * t;
float alpha = CalculateGradientAlpha(t);
// Modify UV calculation to include scroll offset
float u = t + _scrollOffset; // Add scroll offset to U coordinate
// Top vertex
int topVertexIndex = i * 2;
m_Vertices[topVertexIndex].position = new Vector3(x, 0, Vertex.nearZ);
m_Vertices[topVertexIndex].tint = new Color(1, 1, 1, alpha);
m_Vertices[topVertexIndex].uv = new Vector2(u, 1);
// Bottom vertex
int bottomVertexIndex = i * 2 + 1;
m_Vertices[bottomVertexIndex].position = new Vector3(x, height, Vertex.nearZ);
m_Vertices[bottomVertexIndex].tint = new Color(1, 1, 1, alpha);
m_Vertices[bottomVertexIndex].uv = new Vector2(u, 0);
}
// Allocate mesh data and set vertices/indices
MeshWriteData mwd = mgc.Allocate(m_Vertices.Length, m_Indices.Length, texture);
mwd.SetAllVertices(m_Vertices);
mwd.SetAllIndices(m_Indices);
}
private float CalculateGradientAlpha(float t)
{
if (t <= 0.0f)
return 0.0f; // Fully transparent at start
else if (t <= 0.3f)
{
// Linear interpolation from 0 to 1 between 0% and 30%
return Mathf.Lerp(0.0f, 1.0f, t / 0.3f);
}
else if (t <= 0.7f)
{
// Fully opaque between 30% and 70%
return 1.0f;
}
else if (t <= 1.0f)
{
// Linear interpolation from 1 to 0 between 70% and 100%
return Mathf.Lerp(1.0f, 0.0f, (t - 0.7f) / 0.3f);
}
else
return 0.0f; // Fully transparent at end
}
}
Since we now have gradient support in 6.7 alpha 4
Can we also get gradient masking?