Sooo, I added rounded corner feature and moved all skew properties to USS. I am using USS color property for tint/background color, border-radius properties for rounded corners, background-image property for background image. –skew-x for x-directional skew, –skew-y for y-directional skew, –skew-background for skewing or not skewing background image. I am also using transform-origin values for skew point. There is no antialiasing and transition for skew values (I added a simple solution, then removed “_skewX = Mathf.Lerp(_skewX, SkewX, 0.5f);
”). I couldn’t find a solution for these two in a simple way (with using UI Toolkit). By the way, I tried IVisualElementScheduledItem
, but didn’t like the result, lerping is much easier. Also, I just simply stretching background, I don’t need any variation for the background-image.
I used the RoundedQuadMesh sample from Bunny83’s answer and simplified it for what I need.
If I find a way (or if someone helps me) to easily use transition for skew properties in the future, I’d update this post. I think CustomStyleProperty can be automatically support transitions for types like float, int, Color.
using UnityEngine;
using UnityEngine.UIElements;
public class SkewedElement : VisualElement
{
[UnityEngine.Scripting.Preserve]
public new class UxmlFactory : UxmlFactory<SkewedElement, UxmlTraits> {}
static readonly CustomStyleProperty<float> S_SkewX = new CustomStyleProperty<float>("--skew-x");
static readonly CustomStyleProperty<float> S_SkewY = new CustomStyleProperty<float>("--skew-y");
static readonly CustomStyleProperty<bool> S_SkewBG = new CustomStyleProperty<bool>("--skew-background");
public float SkewX { get; private set; }
public float SkewY { get; private set; }
public bool SkewBG { get; private set; }
private Vertex[] _vertices = {};
private ushort[] _indices = {};
public SkewedElement()
{
RemoveBGTint();
generateVisualContent += OnGenerateVisualContent;
RegisterCallback<CustomStyleResolvedEvent>(OnStyleResolved);
}
private void OnStyleResolved(CustomStyleResolvedEvent evt)
{
evt.customStyle.TryGetValue(S_SkewX, out var skewX);
evt.customStyle.TryGetValue(S_SkewY, out var skewY);
evt.customStyle.TryGetValue(S_SkewBG, out var skewBG);
SkewX = skewX;
SkewY = skewY;
SkewBG = skewBG;
RemoveBGTint();
}
private void OnGenerateVisualContent(MeshGenerationContext mgc)
{
var rect = BorderedRect();
if(rect.width < 0.01f || rect.height < 0.01f)
return;
var backgroundColor = resolvedStyle.color;
var radiusTopLeft = resolvedStyle.borderTopLeftRadius;
var radiusTopRight = resolvedStyle.borderTopRightRadius;
var radiusBottomLeft = resolvedStyle.borderBottomLeftRadius;
var radiusBottomRight = resolvedStyle.borderBottomRightRadius;
var biggestRadius = radiusTopLeft;
if(radiusTopRight > biggestRadius)
biggestRadius = radiusTopRight;
if(radiusBottomLeft > biggestRadius)
biggestRadius = radiusBottomLeft;
if(radiusBottomRight > biggestRadius)
biggestRadius = radiusBottomRight;
var cornerSegments = Mathf.Clamp(Mathf.RoundToInt(biggestRadius / 2) + 3, 5, 12);
var vCount = cornerSegments * 4 + 1;
_vertices = new Vertex[vCount];
var f = 1f / (cornerSegments - 1);
var sPI = Mathf.PI * 0.5f * f;
var rW = rect.width * 0.5f;
var rH = rect.height * 0.5f;
var shortest = rW < rH ? rW : rH;
var topLeft = new Vector3(rect.x, rect.y, Vertex.nearZ);
var topRight = new Vector3(rect.width, rect.y, Vertex.nearZ);
var bottomLeft = new Vector3(rect.x, rect.height, Vertex.nearZ);
var bottomRight = new Vector3(rect.width, rect.height, Vertex.nearZ);
var radiusTL = Mathf.Clamp(radiusTopLeft, -shortest, shortest);
var radiusTR = Mathf.Clamp(radiusTopRight, -shortest, shortest);
var radiusBL = Mathf.Clamp(radiusBottomLeft, -shortest, shortest);
var radiusBR = Mathf.Clamp(radiusBottomRight, -shortest, shortest);
_vertices[0].tint = backgroundColor;
_vertices[0].position = new Vector3(rW, rH, Vertex.nearZ);
_vertices[0].uv = new Vector2(rW / rect.width, rH / rect.height);
for(int i = 0; i < cornerSegments; i++)
{
var sin = Mathf.Sin(i * sPI);
var cos = Mathf.Cos(i * sPI);
var v1 = topLeft + new Vector3(radiusTL - cos * radiusTL, radiusTL - sin * radiusTL);
var v2 = topRight + new Vector3(-radiusTR + sin * radiusTR, radiusTR - cos * radiusTR);
var v3 = bottomRight + new Vector3(-radiusBR + cos * radiusBR, -radiusBR + sin * radiusBR);
var v4 = bottomLeft + new Vector3(radiusBL - sin * radiusBL, -radiusBL + cos * radiusBL);
var i1 = 1 + i;
var i2 = cornerSegments + i1;
var i3 = cornerSegments * 2 + i1;
var i4 = cornerSegments * 3 + i1;
_vertices[i1].tint = backgroundColor;
_vertices[i2].tint = backgroundColor;
_vertices[i3].tint = backgroundColor;
_vertices[i4].tint = backgroundColor;
_vertices[i1].position = v1;
_vertices[i2].position = v2;
_vertices[i3].position = v3;
_vertices[i4].position = v4;
if(SkewBG)
{
_vertices[i1].uv = new Vector2(v1.x / rect.width, 1 - v1.y / rect.height);
_vertices[i2].uv = new Vector2(v2.x / rect.width, 1 - v2.y / rect.height);
_vertices[i3].uv = new Vector2(v3.x / rect.width, 1 - v3.y / rect.height);
_vertices[i4].uv = new Vector2(v4.x / rect.width, 1 - v4.y / rect.height);
}
}
var triCount = cornerSegments * 4;
_indices = new ushort[triCount * 3];
for(int i = 0; i < triCount; i++)
{
_indices[i * 3] = 0;
_indices[i * 3 + 1] = (ushort) (i + 1);
_indices[i * 3 + 2] = (ushort) (i + 2);
}
_indices[triCount * 3 - 1] = 1;
SkewRect(rect);
var mwd = mgc.Allocate(_vertices.Length, _indices.Length, resolvedStyle.backgroundImage.texture);
mwd.SetAllVertices(_vertices);
mwd.SetAllIndices(_indices);
}
private void SkewRect(Rect rect)
{
var xOrigin = resolvedStyle.transformOrigin.x / rect.width;
var yOrigin = resolvedStyle.transformOrigin.y / rect.height;
var xSkew = rect.height * Mathf.Tan(Mathf.Deg2Rad * SkewX);
var ySkew = rect.width * Mathf.Tan(Mathf.Deg2Rad * SkewY);
for(int i = 0; i < _vertices.Length; i++)
{
var xLerp = Mathf.LerpUnclamped(0, xSkew, (_vertices[i].position.y - rect.y) / rect.height);
var yLerp = Mathf.LerpUnclamped(0, ySkew, (_vertices[i].position.x - rect.x) / rect.width);
_vertices[i].position += new Vector3(xLerp - xSkew * xOrigin, yLerp - ySkew * yOrigin);
if(!SkewBG)
_vertices[i].uv = new Vector2(_vertices[i].position.x / rect.width, 1 - _vertices[i].position.y / rect.height);
}
}
private Rect BorderedRect()
{
var leftWidth = resolvedStyle.borderLeftWidth;
var topWidth = resolvedStyle.borderTopWidth;
var borderRect = new Rect(new Vector2(-leftWidth, -topWidth), new Vector2(leftWidth + resolvedStyle.borderRightWidth, topWidth + resolvedStyle.borderBottomWidth));
return new Rect(paddingRect.position + borderRect.position, paddingRect.size + borderRect.size);
}
private void RemoveBGTint()
{
var bgTintColor = new StyleColor {value = Color.clear};
style.unityBackgroundImageTintColor = bgTintColor;
}
}
Sample USS code
SkewedElement {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
color: rgb(254, 0, 0);
width: 100px;
height: 100px;
position: absolute;
left: 922px;
top: 230px;
transition-duration: 0.5s;
--skew-x: 10;
--skew-y: 0;
}
SkewedElement:hover {
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
width: 100px;
height: 100px;
color: rgb(217, 217, 217);
--skew-x: 40;
--skew-y: 0;
--skew-background: true;
}