TLabAltoh / Unity-SDF-UI-Toolkit

UI components that render based on SDF. Rounded corners, outlines, SDFTexture editor.
MIT License
25 stars 1 forks source link

SDF Quad is thinner than expected #13

Open AAAYaKo opened 1 month ago

AAAYaKo commented 1 month ago

Comparison of SDF Quad and Unity Image (Rect 5×100) image image Also, with the outline off, there is a thin edge of black color. image You can see better if you turn on outline, set width to 0 and change color.

AAAYaKo commented 1 month ago

image Closest Analog: Outside OutlineColor = FillColor OutlineWidth = 1

It's important for small icons and elements in the form of dots and lines. Need to somehow shift the radius by 1 pixel for the fill and outline.

AAAYaKo commented 1 month ago

Also, when the outline is off or the width of the outline is 0, use fillColor instead of (0,0,0,0).

TLabAltoh commented 1 month ago

Maybe this is due to I have antialiasing in the UI shaders. Unity’s built-in image components are merely quads, but these sdf-quads render rounded corners. Therefore, this feature might be more preferable than a shader without antialiasing.

with antialiasing

// generate UI shape by crop by alpha
float delta = fwidth(dist);
float graphicAlpha = 1 - smoothstep(-_OutlineWidth - delta, -_OutlineWidth, dist);

without antialiasing

float graphicAlpha = 1 - smoothstep(-_OutlineWidth, -_OutlineWidth, dist);

But as you say, it is inconvenient in some cases (like dot rendered game view). Maybe I should seperate shader with antialiasing and non-antialiasing.

AAAYaKo commented 1 month ago

I suggest adding a keyword to the shaders to disable AA for now. And a parameter in the SDFUI. Also, maybe inside/outside can be made via keywords too.

AAAYaKo commented 1 month ago

But still for antialiasing mode it is necessary to add some correction offset, for example 0.5, so that the result would be closer to the display without antialiasing.

AAAYaKo commented 1 month ago

It might be worth trying several antialiasing methods, maybe SubPixel or SuperSampled antialiasing will be more accurate and there will be no need to add an offset. If you find one, keep the current one, because I think it's probably faster and would give you the option to switch between 3 modes: No antialiasing Fast (or if you find a more accurate but faster one) Quality (But it needs to be tested in practice)

Might be useful https://github.com/etienne-p/UnitySDFAntialiasing https://drewcassidy.me/2020/06/26/sdf-antialiasing/

TLabAltoh commented 1 month ago

Thanks for the suggestion. I will implement both subsampling and supersampling for quality mode. Also I'm not well understand to shader keyword but according to the sample you attached, maybe I can replace if else statement in my sdf shader to shader keyword (for INSIDE/OUTSIDE SUB_SAMPLING/SUPER_SAMPLING). So I will implement that too.

TLabAltoh commented 1 month ago

I added the Antialiasing method option. It is difficult to compare the effect of antialiasing by eye, but for subpixel antialiasing, I can see a color gradient at the edge of the shape when scaling up the game view in the editor, so I believe my implementation is correct. This color gradient seems uncomfortable in this attached picture. But it should be effective for antialiasing for the actual display.

~~The color gradient for subpixel antialiasing of this plugin seems thin than sample image of subpixel antialiasing's reference. I think it is because my sampling source is sdf function (not sdf texture), so there is error from actual shape is small than case of sdf texture.~~
Compare antialiasing methods
Previous method (faster method, 1px expanded) Super-sampling-antialiasing Subpixel-antialiasing

I take the above picture from here

AAAYaKo commented 1 month ago

Yes, it's hard for me to evaluate the effect of these changes yet, but I think we need to look into the anti-aliasing some more.

AAAYaKo commented 1 month ago

Maybe problem can be solved by hinting. image https://en.wikipedia.org/wiki/Font_rasterization

AAAYaKo commented 1 month ago

image image image I had certain graphical problems since I'm not a graphical programmer, but I think this direction is right. If you add floor in this place for small rectangles, the results are correct. Perhaps if you find the right place for rounding (or find another way to snap to the pixel grid.) you can get a pixel perfect vector as a result without artifacts.

AAAYaKo commented 1 month ago

image For information during the test I commented out expand code for faster mode, screenshot from super sampling mode. image Faster mode with my rounding looks messy.

TLabAltoh commented 1 month ago

I have look agein my sdf shader and I think my shader is little wong. My previouse implementation, I effect antialiasing from inside direction and maybe this implementation is not standard because most of sample of antialiasing seems bigger than non-antialiased graphic (my antialiased graphic smaller than non-antialiased shape).

I compared antialiasing from inside and outside (Blue is sdf-quad).

From Inside (My previouse implementation) From Outside

If I effect antialiasing from outside, your suggestion to use hinting seems like it will have effect because I can prevent 1px edge expand for sdf shape's edge that does not need antialiasing by that (like the edge part that is not rounded).

If I use antialiasing from the outside, I need to expand the ui mesh a bit (to prevent shape clipping by rect). But I do not need to use offset on shader sampling like in my previous implementation.

TLabAltoh commented 1 month ago

In the latest update I changed the antialiasing direction from inside to outside. And I removed the shader sampling offset to replace it with hinting in the future (hinting is not implemented yet. I will look for a hinting enabled antialiasing implementation).

AAAYaKo commented 1 month ago

image There is now an anti-aliasing artifact.

AAAYaKo commented 1 month ago

image I think this line is redundant.

AAAYaKo commented 1 month ago

image Before image After If we use antialiasing on outside of the edge rectangle looks bigger than expected. I think it's more correct to make antialiasing in the middle of the edge. image Code for sdf quad

AAAYaKo commented 1 month ago

Maybe we should move the antialiasing code to SDFUtils? It looks like copy-paste and maybe it would be better to make a shared function.

TLabAltoh commented 1 month ago

Maybe we should move the antialiasing code to SDFUtils? It looks like copy-paste and maybe it would be better to make a shared function.

Sure it is. we can commonize the process of determine sampling position and process after determined distance from shape. Maybe it is preferable to move this process to other hlsl file and include it to original code.

```hlsl fixed4 frag(v2f i) : SV_Target { /** * determine sampling position */ // ... /** * determine distance from shape */ // ... /** * after determined distance */ #ifdef SDF_UI_AA_SUBPIXEL float4 delta = fwidth(dist) * .5, sdelta = fwidth(sdist) * .5; #elif defined(SDF_UI_AA_SUPER_SAMPLING) || defined(SDF_UI_AA_FASTER) float delta = fwidth(dist) * .5, sdelta = fwidth(sdist) * .5; #else float delta = 0, sdelta = 0; #endif #ifdef SDF_UI_AA_SUBPIXEL float4 graphicAlpha = 0, outlineAlpha = 0, shadowAlpha = 0; #else float graphicAlpha = 0, outlineAlpha = 0, shadowAlpha = 0; #endif #ifdef SDF_UI_OUTLINE_INSIDE graphicAlpha = 1 - smoothstep(-_OutlineWidth - delta, -_OutlineWidth + delta, dist); outlineAlpha = 1 - smoothstep(-delta, delta, dist); shadowAlpha = 1 - smoothstep(_ShadowWidth - _ShadowBlur - sdelta, _ShadowWidth + sdelta, sdist); #elif SDF_UI_OUTLINE_OUTSIDE outlineAlpha = 1 - smoothstep(_OutlineWidth - delta, _OutlineWidth + delta, dist); graphicAlpha = 1 - smoothstep(-delta, delta, dist); shadowAlpha = 1 - smoothstep(_OutlineWidth + _ShadowWidth - _ShadowBlur - sdelta, _OutlineWidth + _ShadowWidth + sdelta, sdist); #endif #ifdef SDF_UI_AA_SUBPIXEL half4 lerp0 = lerp( half4(lerp(half3(1, 1, 1), _OutlineColor.rgb, outlineAlpha.rgb), outlineAlpha.a * _OutlineColor.a), // crop image by outline area color, graphicAlpha // override with graphic alpha ); half4 effects = lerp( half4(_ShadowColor.rgb, shadowAlpha.a * pow(shadowAlpha.a, _ShadowPower) * _ShadowColor.a), lerp0, lerp0.a // override ); #else half4 lerp0 = lerp( half4(_OutlineColor.rgb, outlineAlpha * _OutlineColor.a), // crop image by outline area half4(color.rgb, color.a), graphicAlpha // override with graphic alpha ); half4 effects = lerp( half4(_ShadowColor.rgb, shadowAlpha * pow(shadowAlpha, _ShadowPower) * _ShadowColor.a), lerp0, lerp0.a // override ); #endif effects *= i.color; #ifdef UNITY_UI_CLIP_RECT effects.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect); #endif #ifdef UNITY_UI_ALPHACLIP clip(effects.a - 0.001 > 0.0 ? 1 : -1); #endif return effects; } ``` I can replace above code to bellow ```hlsl fixed4 frag(v2f i) : SV_Target { #include "(determine sampling position).hlsl" /** * determine distance from shape */ // ... #include "(after determined distance).hlsl" } ```

Also, as you say, it seems preferable to antialiase from the both sides of the edge rather than from the outside.

AAAYaKo commented 1 month ago

As written in this article fwidth is an expensive operation that you should not call too much. Do we need to calculate sdelta in case the shadow is off? http://www.numb3r23.net/2015/08/17/using-fwidth-for-distance-based-anti-aliasing/ Screenshot_20240812_193144_Edge

AAAYaKo commented 1 month ago

Maybe calculations related with shadow also should be hidden behind a keyword?

TLabAltoh commented 1 month ago

I see, seen the article. Certnly, it is overhead that culclate shadow's delta in case of no used. Need to add keyword to shader's to switch shadow process enable/disable. Currently I am doing to remove tiny black color from shape edge. I will push update once this and move shadow related process to keyword's section is done.

AAAYaKo commented 1 month ago

I suggest changing sample positions for SSAA. https://www.beyond3d.com/content/articles/122/8 According to this article sparse grid anti-aliasing position of the samples is the most efficient one.

TLabAltoh commented 1 month ago

I accidentally referenced a different issue id in the commit message. So I created this message to announce my update. 4f68e7f

TLabAltoh commented 1 month ago

In the previous update, there is a problem in shader that runtime attached sdf-ui is not rendered in build-app. Maybe this is because I use shader keyword as shader_feature instead of multi_compile, my shader's keyword branch is too complex for compiler, so there are some critical part for shader has dropped.

In the latest update, I replace all shader keyword from shader_feature to multi_compile.

Multi-compile keyword increases build time and build app size (but build time will not increase if build cache remains after first build). I may need to change some keywords back from multi_compile to shader_feature that is not seem to cause the above problem.

I have made a notice of this as it is a critical issue. 330555a


PS: I have misunderstood cause of problem.

shader_feature and multi_compile mostly behave the same, but shader_feature are ignoring compile for keyword branch that is not used when building. I put shader in resources folder to always have refarence of it, but it is not enough to enable shader_feature in compile.

If all shader_feature branch is compiled as same as multi_compile, build time and app size is same.

So my latest update to replace shader_feature with multi_compile seems not wrong, but in order to decrease build time and app size, I have to optimize my shader code (not keyword branch).

AAAYaKo commented 1 month ago

image As I understand it with 2 passes in Build-In RP, antialiasing doesn't work. image We need to find out how antialiasing works in TMP. Maybe their implementation doesn't involve 2 passes, but I could be wrong.

TLabAltoh commented 1 month ago

(I deleted this message becuse of my miss understanding)

TLabAltoh commented 1 month ago

For antialiasing problem, I remaked sdf shader (not pushed yet because add shadow in single pass rendering still remaine) with reference to TMPro's shader (TMP_SDF.shader and TMP_SDF-Mobile.shader). I just changed bellow.

// Unity-SDF-UI-Toolkit\Runtime\Resources\Shader\SDF-UI\{Shape-Name}.shader

// Befor
Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha

// After
Blend One OneMinusSrcAlpha
// Unity-SDF-UI-Toolkit\Runtime\Resources\Shader\SDF-UI\ClipByDistance.hlsl

// Befor
#ifdef SDF_UI_AA_SUBPIXEL
half4 layer0 = lerp(half4(1, 1, 1, 0), _OutlineColor, outlineAlpha);
half4 layer1 = color * graphicAlpha;
#else
half4 layer0 = half4(_OutlineColor.rgb, _OutlineColor.a * outlineAlpha);
half4 layer1 = half4(color.rgb, color.a);
#endif
half4 layer2 = blend(layer0, layer1, _OutlineColor, color, graphicAlpha);

half4 effects = layer2;

// After
half4 layer0 = _OutlineColor; layer0.rgb *= layer0.a;
half4 layer1 = color; layer1.rgb *= layer1.a;
half4 layer2 = lerp(layer0, layer1, graphicAlpha);
half4 effects = layer2 * outlineAlpha;

Here is the result (blue color is the background)

Due to the screen capture problem, the color gradation between red and blue appears whitish, but the actual display works fine.

I have not seen the whole code, so maybe there are some of my missed.

TLabAltoh commented 4 weeks ago

I also added the shadow in the same pass of the shape and outline. The shader is now rendered in a single pass. Currently, it should be fixed that antialiasing doesn't work when the outline width is zero. Currently I only referenced the implementation from TMPro's shader that related to this plugin's issue about antialiasing and multipass. So maybe there are others that are useful to reference in other parts of TMPro's shader.

AAAYaKo commented 4 weeks ago

If so, I think this section can be removed from the readme Screenshot_20240817_221858_Edge

TLabAltoh commented 4 weeks ago

Yes, it is. This section is no longer needed.

AAAYaKo commented 4 weeks ago

I also added the shadow in the same pass of the shape and outline. The shader is now rendered in a single pass. Currently, it should be fixed that antialiasing doesn't work when the outline width is zero. Currently I only referenced the implementation from TMPro's shader that related to this plugin's issue about antialiasing and multipass. So maybe there are others that are useful to reference in other parts of TMPro's shader.

I think we need to copy the shadow settings. Current Power, Width, Blur are not very clear and do not look like other programs. In opposition to this, TMP gives quite typical settings. image I would also like to be able to use HDR colors like in TMP.

TLabAltoh commented 4 weeks ago

Replaced shadow parameters to similar for TMPro's it and supported HDR in color properties. Currently, I left the previous soft shadow implementation in the shader (not similar to TMPro). TMPro uses only distance from shape and saturate(), so soft shadow seems blur linearly. This plugin's previouse blur implementation is based on this (smoothstep() based) and I think this is looks great than TMPro's linerly soft shadow (but it is problem that soft shadow result is smaller as much as to increase softness value than actual size).

AAAYaKo commented 3 weeks ago

fixed newtonsoft miss reference

AAAYaKo commented 3 weeks ago

image image

AAAYaKo commented 3 weeks ago

Maybe it's worth placing shaders in the Hidden group, like the default and tmp shaders?

AAAYaKo commented 6 days ago

I found that when this setting is disabled, shaders don't compile. It is worth adding this to the readme. image

AAAYaKo commented 6 days ago

Also I think 6000+ shader variants is too much. It would be nice to find a solution to this issue. Maybe we should give up keyword in certain places and return if logic. Maybe we should look at how disabling properties in TMP works.

TLabAltoh commented 6 days ago
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP

#pragma multi_compile_local _ SDF_UI_AA_FASTER
#pragma multi_compile_local _ SDF_UI_AA_SUPER_SAMPLING
#pragma multi_compile_local _ SDF_UI_AA_SUBPIXEL

#pragma multi_compile_local _ SDF_UI_ONION

#pragma multi_compile_local _ SDF_UI_OUTLINE_INSIDE
#pragma multi_compile_local _ SDF_UI_OUTLINE_OUTSIDE

#pragma multi_compile_local _ SDF_UI_SHADOW_ENABLED

I see, above is a shader keyword currently in use, it is ideal to leave only keyword for antialiasing option (SDF_UIAA*).

// ideal
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP

#pragma multi_compile_local _ SDF_UI_AA_FASTER SDF_UI_AA_SUPER_SAMPLING SDF_UI_AA_SUBPIXEL
TLabAltoh commented 6 days ago

For now, I leave all shader keyword, but changed layout to prevent generating unintended variant.

#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP

#pragma multi_compile_local _ SDF_UI_AA_FASTER SDF_UI_AA_SUPER_SAMPLING SDF_UI_AA_SUBPIXEL

#pragma multi_compile_local SDF_UI_OUTLINE_INSIDE SDF_UI_OUTLINE_OUTSIDE

#pragma multi_compile_local _ SDF_UI_ONION

#pragma multi_compile_local _ SDF_UI_SHADOW_ENABLED
AAAYaKo commented 5 days ago

image Already much better, but still about 800 variants sound like a lot.

TLabAltoh commented 5 days ago

Keyword that start with SDF_UI_OUTLINE* is used only here.

// 1.
#ifdef SDF_UI_OUTLINE_INSIDE
tmp0 = 1 - saturaterange((_ShadowWidth - _ShadowDilate) - _ShadowBlur - delta, (_ShadowWidth - _ShadowDilate) + delta, dist);
tmp1 = 1 - smoothstep((_ShadowWidth - _ShadowDilate) - _ShadowBlur - delta, (_ShadowWidth - _ShadowDilate) + delta, dist);
#elif SDF_UI_OUTLINE_OUTSIDE
tmp0 = 1 - saturaterange(_OutlineWidth + (_ShadowWidth - _ShadowDilate) - _ShadowBlur - delta, _OutlineWidth + (_ShadowWidth - _ShadowDilate) + delta, dist);
tmp1 = 1 - smoothstep(_OutlineWidth + (_ShadowWidth - _ShadowDilate) - _ShadowBlur - delta, _OutlineWidth + (_ShadowWidth - _ShadowDilate) + delta, dist);
#endif

// 2.
#ifdef SDF_UI_OUTLINE_INSIDE
graphicAlpha = 1 - saturaterange(-_OutlineWidth - delta, -_OutlineWidth + delta, dist);
outlineAlpha = 1 - saturaterange(-delta, delta, dist);
#elif SDF_UI_OUTLINE_OUTSIDE
outlineAlpha = 1 - saturaterange(_OutlineWidth - delta, _OutlineWidth + delta, dist);
graphicAlpha = 1 - saturaterange(-delta, delta, dist);
#endif

Maybe replacing here to properties can reduce variant to half (400) without adding if statement.

// 1. Add _ShadowBorder
tmp0 = 1 - saturaterange(_ShadowBorder - _ShadowBlur - delta, _ShadowBorder + delta, dist);
tmp1 = 1 - smoothstep(_ShadowBorder - _ShadowBlur - delta, _ShadowBorder + delta, dist);

// 2. Add _OutlineBorder
outlineAlpha = 1 - saturaterange(_OutlineBorder - delta, _OutlineBorder + delta, dist);
graphicAlpha = 1 - saturaterange(-delta, delta, dist);
TLabAltoh commented 5 days ago

Shader variant optimization