com.openugd.ui adds three components to uGUI (Unity UI) that need no shader, material or texture of their own:
two mesh effects that change the mesh a graphic already builds (UIFlippable mirrors it, GradientMeshEffect
colours it with a gradient) and EmptyGraphic, an invisible raycast target that builds no mesh. Use it when an
Image, Text or other uGUI graphic needs a flip without a negative scale, a gradient without a gradient
texture, or a clickable area without a transparent Image.
All three live in the OpenUGD.UI namespace. The package depends only on com.unity.ugui; it needs no other
OpenUGD package, and no OpenUGD package needs it.
openupm add com.openugd.ui@2.0.0Add the registry and the package to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": [
"com.openugd"
]
}
],
"dependencies": {
"com.openugd.ui": "2.0.0"
}
}In Packages/manifest.json:
{
"dependencies": {
"com.openugd.ui": "https://github.com/openugd/upm-ui.git#2.0.0"
}
}A git URL does not resolve OpenUGD dependencies from OpenUPM, but this package has none: the line above is
all a git install needs. Unity resolves com.unity.ugui from its own registry.
- Unity 6000.0 or newer.
- uGUI
com.unity.ugui2.0.0 or a later 2.x, which Unity 6 projects include by default. - No other OpenUGD package.
In the editor the components are under Add Component > UI: Empty Graphic, and Effects > Flippable and Effects > Gradient next to Unity's Shadow and Outline. From code:
using OpenUGD.UI;
using UnityEngine;
using UnityEngine.UI;
public class GradientButtonSetup : MonoBehaviour
{
[SerializeField] private Image _target;
private void Start()
{
// Mirror the image left to right; the RectTransform keeps its positive scale.
var flippable = _target.gameObject.AddComponent<UIFlippable>();
flippable.horizontal = true;
// A vertical gradient multiplied into the image's colour. Effects run in component order, so this one
// runs after the flip and keeps its direction.
var gradient = _target.gameObject.AddComponent<GradientMeshEffect>();
gradient.GradientType = GradientMeshEffect.Type.Vertical;
gradient.BlendMode = GradientMeshEffect.Blend.Multiply;
gradient.GradientColor = new Gradient
{
colorKeys = new[]
{
new GradientColorKey(Color.white, 0f),
new GradientColorKey(Color.gray, 1f)
},
alphaKeys = new[]
{
new GradientAlphaKey(1f, 0f),
new GradientAlphaKey(1f, 1f)
}
};
// An invisible, clickable area over the image: no vertices, still a raycast target.
var hitArea = new GameObject("HitArea", typeof(RectTransform), typeof(EmptyGraphic), typeof(Button));
hitArea.transform.SetParent(_target.transform, false);
var area = (RectTransform)hitArea.transform;
area.anchorMin = Vector2.zero;
area.anchorMax = Vector2.one;
area.sizeDelta = Vector2.zero;
hitArea.GetComponent<Button>().onClick.AddListener(() => Debug.Log("clicked"));
}
}Every setter rebuilds the graphic's mesh when the value changes (GradientColor on every assignment), so
animating GradientOffset or toggling horizontal needs no SetVerticesDirty call. Like every uGUI
component, the three are used from Unity's main thread.
uGUI runs the mesh effects on a GameObject in the order of its components, top to bottom in the Inspector.
UIFlippable above GradientMeshEffect mirrors the graphic and leaves the gradient's direction alone; below
it, the gradient is mirrored with the graphic. The same holds for Unity's Shadow and Outline. Neither
component reorders itself. A disabled or inactive effect leaves the mesh alone, and enabling or disabling it
rebuilds the mesh.
UIFlippable moves each vertex to the other side of the centre of the RectTransform rect. The texture is
mirrored with the geometry; the transform, the layout and the raycast area stay as they are. Unlike a negative
scale, it mirrors about the centre of the rect whatever the pivot, and it leaves the children alone. Mirroring
on one axis reverses the triangle winding, as a negative scale does; uGUI's default shader draws both faces.
var flip = icon.gameObject.AddComponent<UIFlippable>();
flip.horizontal = true; // mirror left to right
flip.vertical = true; // and top to bottom: together, a half turn about the centreShape and layout. GradientType is Horizontal, Vertical, Radial or Diamond. All four are laid out
on the bounds of the mesh's vertices, not around the pivot. At zoom 1 and offset 0, Horizontal and Vertical
run from one edge (0) to the other (1); Radial reaches 1 on the ellipse inscribed in the bounds, Diamond on
the diamond whose corners touch the middle of each edge.
Zoom and offset. GradientZoom (0.1 to 10) magnifies the gradient: Horizontal and Vertical about the
middle of the bounds, Radial and Diamond from the centre. Below 1 the gradient ends inside the bounds and its
end colours fill the rest. GradientOffset (-1 to 1) slides it; for Radial and Diamond a positive offset
pushes the colours outwards. Both setters clamp.
Blend. BlendMode decides how the gradient combines with the colour the vertex already has (the
graphic's color, or the result of an earlier effect): Override replaces it, Add adds per channel, and
Multiply (the default) multiplies per channel, alpha included.
Extra vertices. A GPU interpolates vertex colours linearly across each triangle, so a plain quad cannot
show a key between the ends, and a Radial or Diamond gradient would give all four corners the same colour.
ModifyVertices (on by default) cuts the triangles where the gradient needs vertices: at every colour and
alpha key, and around the centre for Radial and Diamond. The outline is kept and every new vertex is
interpolated from the triangle it was cut from, UVs included, so sliced, tiled and atlas sprites keep their
texturing. With the Gradient's default mode (GradientMode.Blend), Diamond and the linear shapes come out
exact; Radial is split into 32 wedges and stays within 0.5% of the true distance. Turn ModifyVertices off
for meshes that are already dense, such as text. A mesh that would reach uGUI's 65,000-vertex limit is
coloured without extra vertices instead.
Changing a gradient in place. GradientColor returns the Gradient instance the component holds, and the
setter keeps the instance it is given, not a copy. After changing its keys, assign it back: the setter
refreshes the key positions the effect has cached and rebuilds the mesh. SetVerticesDirty() alone rebuilds
with the colours of the new keys but the vertices of the old ones. Detecting the change on every rebuild would
need Gradient.Equals, which allocates in the Unity runtime. The cache is also refreshed when the component is
enabled and when it is edited in the Inspector. Effects given the same instance share it, so an in-place change
reaches all of them and must be assigned back to each.
using OpenUGD.UI;
using UnityEngine;
public class GradientRecolour : MonoBehaviour
{
[SerializeField] private GradientMeshEffect _effect;
public void SetColours(Color start, Color middle, Color end)
{
var gradient = _effect.GradientColor;
gradient.SetKeys(
new[]
{
new GradientColorKey(start, 0f),
new GradientColorKey(middle, 0.5f),
new GradientColorKey(end, 1f)
},
gradient.alphaKeys);
// Required: the setter re-reads the keys and rebuilds the mesh.
_effect.GradientColor = gradient;
}
}Allocation. Once warm, a rebuild allocates no managed memory: the work lists come from Unity's
ListPool, and the key positions are re-read only after the gradient is assigned, the component is enabled
or it is edited in the Inspector.
Tangents. The Inspector-only option Modify Tangents writes the blended colour into the vertex tangent instead of the colour, for a custom shader that reads it there. The vertex colour is then left unchanged.
EmptyGraphic builds a mesh with no vertices, so nothing is drawn, and accepts every raycast location. The
hit area is the RectTransform rect, as for any graphic: GraphicRaycaster tests the rect, then asks
ICanvasRaycastFilter components. raycastTarget turns it off. Use it as a Button's target graphic (with
Transition set to None), a drag handle or a click blocker, instead of an Image with zero alpha, which
still builds a mesh and is drawn unless its CanvasRenderer culls transparent meshes.
var blocker = new GameObject("ClickBlocker", typeof(RectTransform), typeof(EmptyGraphic));
blocker.transform.SetParent(canvas.transform, false); // stretch it over what it should block| Type | Base | Purpose |
|---|---|---|
UIFlippable |
BaseMeshEffect |
Mirrors the graphic's mesh about the centre of its RectTransform rect. |
GradientMeshEffect |
BaseMeshEffect |
Writes a Gradient into the graphic's vertex colours. |
GradientMeshEffect.Type |
enum : byte |
Horizontal, Vertical, Radial (elliptical distance from the centre), Diamond (Manhattan distance from the centre). |
GradientMeshEffect.Blend |
enum : byte |
Override, Add, Multiply: how the gradient combines with the vertex colour. |
EmptyGraphic |
Graphic, ICanvasRaycastFilter |
Draws nothing and receives raycasts over its whole rect. |
| Member | Type | Default | Contract |
|---|---|---|---|
UIFlippable.horizontal |
bool |
false |
Mirror left to right. Rebuilds the mesh when the value changes. |
UIFlippable.vertical |
bool |
false |
Mirror top to bottom. Rebuilds the mesh when the value changes. |
GradientMeshEffect.GradientType |
Type |
Horizontal |
The shape. Rebuilds the mesh when the value changes. |
GradientMeshEffect.BlendMode |
Blend |
Multiply |
How the colours combine. Rebuilds the mesh when the value changes. |
GradientMeshEffect.GradientColor |
Gradient |
black to white | Returns the instance the component holds; the setter stores the reference, not a copy. Assign it back after changing it in place. Every assignment rebuilds the mesh. null throws ArgumentNullException. |
GradientMeshEffect.GradientOffset |
float |
0 |
Slides the gradient; clamped to -1..1. Rebuilds the mesh when the value changes. |
GradientMeshEffect.GradientZoom |
float |
1 |
Magnifies the gradient; clamped to 0.1..10. Rebuilds the mesh when the value changes. |
GradientMeshEffect.ModifyVertices |
bool |
true |
Adds the vertices the gradient needs. Rebuilds the mesh when the value changes. |
UIFlippable.ModifyMesh, GradientMeshEffect.ModifyMesh |
void (VertexHelper) |
Called by the graphic while it rebuilds its mesh; does nothing while the component is disabled or inactive. | |
EmptyGraphic.IsRaycastLocationValid |
bool (Vector2, Camera) |
Always true: the rect test has already passed. |
Modify Tangents is a serialized field without a property.
Import from Package Manager > OpenUGD uGUI Components > Samples:
| Sample | What it shows |
|---|---|
| Components Demo | One canvas built in code: text mirrored four ways plus a copy that flips every second, the four gradient shapes (Horizontal animating its offset) plus Radial again on a graphic with its pivot in the corner, and an invisible button whose hit area is an EmptyGraphic. Put UIComponentsDemo on an empty GameObject and enter Play mode; add an Event System to click the button. |
The EditMode tests are in the assembly com.openugd.ui.tests. Unity compiles a package's tests only when the
package is listed under testables in Packages/manifest.json:
{
"dependencies": {
"com.openugd.ui": "2.0.0"
},
"testables": [
"com.openugd.ui"
]
}Then open Window > General > Test Runner, choose EditMode and run com.openugd.ui.tests. The project
needs the Test Framework package (com.unity.test-framework), which new Unity 6 projects include. There are
no PlayMode tests.
The tests of the components themselves carry [Category("RequiresUnity")]. The rest cover the mesh
algorithms behind them (mirroring, triangle cutting, gradient layout and tessellation), which make no engine
calls and also run on .NET without the editor: level1.sh in
openugd/upm-tools runs them with dotnet test.
The checks also run in public CI: openugd/upm-tools
compiles this package, its samples and the complete examples in this README (those that declare a type) against
Unity 6000.0's assemblies and runs its engine-free tests on every change there and every Monday. The tests that need
the editor (category RequiresUnity) run in a real Unity 6000.0.41f1 editor before each release.
2.0.0 follows 0.1.1; there is no 1.x. The package jumped to 2 to share the major version of the OpenUGD 2.0 family. What a 0.1.1 project meets, and what to do:
| What | 0.1.1 | 2.0.0 | What to do |
|---|---|---|---|
| Namespace | UnityEngine.UI |
OpenUGD.UI |
Add using OpenUGD.UI; to scripts that name the components. |
| Scenes and prefabs | bind by script GUID | bind by the same GUIDs | Nothing. |
| Minimum Unity | 2021.3 | 6000.0 | Stay on 0.1.1 on older editors. |
UIFlippable order |
moved itself above the other mesh effects | stays where you put it | Order the effects in the Inspector. |
Radial with ModifyVertices |
mesh replaced by an ellipse | outline and UVs kept | Use a round sprite or a Mask for a round shape. |
| Diamond | a circle scaled by the height, centred only for a middle pivot | Manhattan distance from the centre | Check every Diamond gradient. |
| Gradient changed in place | keys re-read on every rebuild | keys re-read when it is assigned back | Assign it back to GradientColor. |
GradientColor = null |
accepted, failed at the next rebuild | ArgumentNullException |
Assign a Gradient. |
| Menu | UI > EmptyGraphic | UI > Empty Graphic | Nothing. |
| Licence | MIT | Apache-2.0 | See LICENSE.md. |
The three components moved from UnityEngine.UI to OpenUGD.UI. Keep using UnityEngine.UI; for Image,
Graphic and the rest of uGUI:
using UnityEngine;
using UnityEngine.UI; // Image, Button, Graphic
+using OpenUGD.UI; // UIFlippable, GradientMeshEffect, EmptyGraphicA fully qualified name changes the same way: UnityEngine.UI.GradientMeshEffect.Type.Radial is now
OpenUGD.UI.GradientMeshEffect.Type.Radial. The assembly is still com.openugd.ui, so asmdef references need
no change.
Scenes and prefabs need nothing. Unity binds a component to its script by the script's GUID, not by its
namespace, and the file names, class names and .meta GUIDs are unchanged. So are the serialized field names,
except UIFlippable's misspelt _veritical, now _vertical; [FormerlySerializedAs] reads the old name, so
saved values survive, and a scene saved with 2.0 writes the new one.
Each type carries [MovedFrom(true, sourceNamespace: "UnityEngine.UI")], which asks Unity's API Updater to
rewrite old references when it runs on a script that no longer compiles. If it leaves a script unchanged, add
the using line by hand.
In the editor, whenever it woke or was validated, 0.1.1 moved UIFlippable up the component list, one slot for
every mesh effect above it, and it mirrored the mesh even while disabled. In 2.0 it does neither. To keep the 0.1.1
result, drag it above the other effects once and save the scene or prefab; to mirror another effect's result
too, put it below.
The setters now rebuild the mesh themselves:
// 0.1.1: the setter only stored the value.
flippable.horizontal = true;
image.SetVerticesDirty();
// 2.0: the setter rebuilds the mesh when the value changes. The extra call still works but is not needed.
flippable.horizontal = true;A subclass that overrode UIFlippable's editor-only Awake() or OnValidate() now overrides
UIBehaviour.Awake and BaseMeshEffect.OnValidate instead; calls to base.Awake() and base.OnValidate()
still compile.
Radial. With ModifyVertices on, 0.1.1 replaced the whole mesh with a 64-segment ellipse the size of the
vertex bounds, laid out around the pivot, with UVs from 0 to 1. Rectangular graphics were cropped, sliced and
tiled geometry was lost, atlas sprites showed the wrong part of the atlas, and the shape was off-centre for any
pivot other than the middle. 2.0 cuts the existing triangles instead, into 32 wedges around the centre and
along a ring at every key, so the graphic keeps its outline and its sprite's UVs, centred whatever the pivot.
The colour at a given point is computed as before, so with ModifyVertices off a Radial gradient looks as it
did (unless Modify Tangents is on, below). For a round shape, use a round sprite or a Mask.
Diamond. 0.1.1 measured the straight-line distance, a circle rather than a diamond, from the point
(c / 2, c / 2), where c is the y coordinate of the centre of the vertex bounds, and divided it by the height
of the bounds alone. That point is the centre only when the bounds are centred on the pivot, as they are for an
Image with a middle pivot, and at zoom 1 the gradient's end lay one full height away from it. 2.0 measures
the Manhattan distance from the centre of the vertex bounds and reaches the gradient's end on the diamond whose
corners touch the middle of each edge. Every Diamond gradient looks different; recheck its keys and zoom. For
a circular falloff use Radial.
Modify Tangents now applies to Radial and Diamond as well; 0.1.1 ignored it there and wrote the vertex colour. If it is on for a Radial or Diamond gradient, turn it off to keep colouring through the vertex colour.
Horizontal and Vertical compute each vertex's colour as 0.1.1 did. The vertices ModifyVertices adds at a key
were white in 0.1.1, so with Multiply or Add they lost the graphic's colour; they now take it from the
triangle they were cut from.
GradientMeshEffect now caches where its gradient's keys are, because reading them allocates. An in-place
change is picked up when the gradient is assigned back:
// 0.1.1: every rebuild re-read the keys.
effect.GradientColor.SetKeys(colorKeys, alphaKeys);
image.SetVerticesDirty();
// 2.0: assigning the gradient back re-reads the keys and rebuilds the mesh.
var gradient = effect.GradientColor;
gradient.SetKeys(colorKeys, alphaKeys);
effect.GradientColor = gradient;Assigning null now throws ArgumentNullException; 0.1.1 accepted it and threw a NullReferenceException at
the next rebuild.
- The setters of both effects rebuild the mesh only when the value changes (
GradientColoron every assignment). To force a rebuild, callSetVerticesDirty()on the graphic. - A mesh that
ModifyVerticeswould grow to 65,000 vertices or more, where uGUI throws, is coloured without extra vertices. - The display name is "OpenUGD uGUI Components" instead of "UI Elements", the former name of UI Toolkit. The
package ID
com.openugd.uiis unchanged. - UI > Effects > Flippable and UI > Effects > Gradient keep their paths and now sort after Unity's own effects.
package.jsondeclarescom.unity.ugui2.0.0; 0.1.1 declared no dependency.
The complete list is in CHANGELOG.md.
Six packages, versioned together as 2.x and published on OpenUPM
under the com.openugd scope. Installing one brings the ones it depends on.
| Package | What it gives you | Depends on |
|---|---|---|
Lifetime — com.openugd.lifetime |
Scopes with deterministic, reverse-order clean-up | — |
Signal — com.openugd.signal |
Typed events whose subscriptions end with a lifetime | Lifetime |
Context — com.openugd.context |
Dependency injection that validates the whole graph before it builds anything | Lifetime |
CoreLib — com.openugd.corelib |
The Unity boundary: ContextBehaviour, presenters, commands, logging |
Lifetime, Signal, Context |
CoreLib uGUI Presenters — com.openugd.corelib.widgets |
Presenters that bind uGUI and TextMesh Pro controls to a model | CoreLib, Context, Signal, Lifetime, uGUI |
uGUI Components — com.openugd.ui |
Shader-free uGUI components: flip, gradient, invisible hit area | uGUI |
Start with Lifetime and Signal for plain C# scopes and events, add Context for dependency injection, and CoreLib to
run it inside a Unity scene. com.openugd.configuration, a
string-keyed configuration for Context, is 0.x and not on OpenUPM yet. Other com.openugd.* packages on OpenUPM
predate 2.0 and are not part of this family.
The OpenUGD packages share their major version; minor and patch versions are independent. Each 2.x package
works with the 2.x versions of its dependencies at or above the minimums declared in its package.json.
com.openugd.ui has no OpenUGD dependency and no OpenUGD package depends on it, so it can be updated on its
own.
The changes in each version are listed in CHANGELOG.md.
Report a bug or an idea at github.com/openugd/upm-ui/issues: include the
Unity version, the package version and, for an exception, the full message. To work on the package, clone it,
reference the clone from a Unity 6 project ("com.openugd.ui": "file:../path/to/upm-ui" in
Packages/manifest.json), add com.openugd.ui to testables, and run its tests in the Test Runner. The checks
CI runs are scripts in openugd/upm-tools; its README shows how to run them
locally.
Apache-2.0 — see LICENSE.md. Releases before 2.0.0 remain under the terms they were published with.