custom shaders¶
Container has two escape hatches for painting beyond the built-in style properties: render it with a custom shader through the Effect channel, or take over drawing with a Draw callback. reach for these only when the style facade cannot express what you want.
the Effect channel¶
set Effect to a ShaderEffect: point it at a compiled ui_*.shader and set the shader's uniforms by name. there is no separate uniform array to keep in sync and no material to cache by hand - ShaderEffect reads the shader's own parameter schema, so a misspelled uniform is caught and logged rather than silently bound.
using Goo;
new Container
{
Width = 256, Height = 256,
BorderRadius = Length.Percent( 50 ),
Effect = new ShaderEffect( "shaders/ui_frosted.shader", GrabMode.Blurred )
{
["CornerRadius"] = 25f,
["Milkiness"] = 0.35f,
["Blur"] = 3f,
},
}
the second constructor argument is the framebuffer grab the effect needs before it draws: GrabMode.None for procedural shaders, GrabMode.Sharp to read the raw backdrop, or GrabMode.Blurred for a gaussian-blurred copy (frosted glass, aero). the first argument can also be a Shader resource instead of a path: new ShaderEffect( MyShader ) with a [Property] public Shader MyShader gives you a drag-droppable slot in the inspector.
a uniform value is either a literal or a Func<T> evaluated every frame:
Effect = new ShaderEffect( "shaders/ui_spotlight.shader" )
{
["LightPos"] = (Func<Vector2>)( () => Cursor01 ),
["Radius"] = 0.22f,
},
int values coerce to float automatically (["Levels"] = Levels with an int property, no (float) cast needed, and an enum needs a single (int) cast). for a per-frame time uniform, use UniformValue.Time instead of hand-rolling a Func<float> - it is a single shared instance, so the bag still compares equal across rebuilds. most UI shaders read the global g_flTime on the GPU and need no CPU-side time at all. reach for UniformValue.Time only when a shader declares its own time attribute.
["MatrixSize"] = (int)Matrix, // enum: one cast, not (float)(int)
["AuraTime"] = UniformValue.Time, // per-frame Time.Now push
goo does not ship shaders. the .shader an effect points at is your project's own asset: author it under Assets/shaders/ in your addon and reference it by its mounted path (shaders/ui_frosted.shader above names a shader from goo's demo project, not a library file). what the library provides is the application machinery: material caching, uniform validation against the shader's declared parameters, the per-frame push, and the framebuffer grab.
one piece of that machinery is worth knowing about. all panels share a single attribute namespace on the render command list, so a uniform set by one panel would otherwise bleed into every later panel drawing with the same shader. ShaderEffect prevents this by resetting any uniform it does not set itself back to the shader's declared Default(). give every tunable uniform a Default() in its declaration and two panels with the same shader stay independent.
ShaderEffect is a record keyed on (shader, grab, uniform bag): two effects with equal bags compare equal, so goo skips re-applying an unchanged effect. construct a fresh one in Build(), and do not hold instances across rebuilds. (a Func uniform makes every rebuild compare unequal - which is exactly what you want for an animating value.)
full-screen effects¶
to run an effect over the whole view - a post-process style look that re-renders everything behind the UI through your shader - put the Effect on a full-bleed container with GrabMode.Sharp (or Blurred). the safe host is the root your Build() returns, sized 100% x 100%: a grab effect draws a quad the size of its panel's layout box, so it needs a definite, non-zero box. an absolutely-positioned child whose only content is the effect can collapse to zero size and silently draw nothing. when a known-good shader shows a blank screen, suspect the host panel's box before the shader.
writing your own effect¶
most effects need no C# at all - author a ui_*.shader and point a ShaderEffect at it. reach for a subclass only when an effect needs bespoke per-frame CPU logic a Func uniform can't express. then subclass ShaderEffect, back Material with a static Material.FromShader (loaded once), and set the shader attributes in Apply (call base.Apply first to get BoxSize set):
public sealed record Ripple : ShaderEffect
{
static readonly Material Shader = Material.FromShader( "shaders/ui_ripple.shader" );
public float Strength { get; init; } = 1f;
public override Material Material => Shader;
protected internal override void Apply( CommandList cl, Rect rect )
{
base.Apply( cl, rect );
cl.Attributes.Set( "Strength", Strength );
}
}
goo previously shipped a record per effect this way. the generic channel above replaced them all, because it reads the shader's own schema instead of restating it in C#. treat the subclass as a last resort.
notes for the shader itself:
g_flTimeis a global uniform in every UI shader, so an effect can animate on the GPU with no per-frame C#.- to read the backdrop, call
cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture", Graphics.DownsampleMethod.GaussianBlur )and sample ati.vTexCoord.zw. - the custom-draw path does not set the engine blend combo: declare your own blend
RenderState(straight alpha) and multiply output byi.vColor.afor CSS opacity.
the Draw callback¶
Draw is a DrawCallback(Canvas) for issuing batched primitive draws when neither the style facade nor an effect fits:
new Container { Width = 120, Height = 120, Draw = canvas => { /* canvas.Rect(...) */ } }
Effect and Draw are independent channels. a container can use either, both, or neither.
known limitation: transformed ancestors¶
both channels render through the panel's own paint pass, which does not compose parent transforms the way the batched background path does. under a rotated or scaled ancestor (or a centered-flex chain) an effect or custom-draw panel can paint at the wrong position. keep these panels at the root or in an untransformed part of the tree.
see also¶
- container-reference - the
EffectandDrawproperties on the full container surface - overlay-layout -
Hud.Fill()and the full-bleed layers a screen-wide effect sits on