Unity fixed blurred text

461 views
Skip to first unread message

baop...@gmail.com

unread,
Feb 8, 2017, 6:36:22 PM2/8/17
to Developer Support
Hello there,

Thank you for the great plug-in. I have noticed that Unity has fixed their blurred text which requires to downscale the Text component and increase the font size to make it look sharp. However, the text in your plug-in is still somewhat blurred, so I just wonder if the Unity fix helps to improve this issue in your plug-in at all.

Thank you.

Developer Support

unread,
Feb 8, 2017, 7:48:57 PM2/8/17
to Developer Support
Hi again! I actually recently did a pretty thorough pass to make sure HyperText was synchronized with the latest changes in the built-in Text component, but it's possible I missed something. If you have a scene that clearly shows this difference could you please send it to bu...@candlelightinteractive.com? Thanks!

mov...@gmail.com

unread,
Mar 7, 2017, 4:28:00 PM3/7/17
to Developer Support
Text is blurry in Torment: Tides of Numenera on resolutions below 4k. The problem has been acknowledged by a InXile, but they haven't released a fix yet.

In the meantime, a user named Crashed has released a patch:

http://steamcommunity.com/app/272270/discussions/1/133258092256946326/?ctp=5

"For the technical: this patches Candlelight.UI.HyperText.pixelsPerUnit to return a constant 1.0, instead of calculating a value based on canvas.scaleFactor."

Unity 5.6 uses this code in UnityEngine.UI.dll:

public float pixelsPerUnit
{
get
{
Canvas canvas = base.canvas;
float result;
if (!canvas)
{
result = 1f;
}
else if (!this.font || this.font.dynamic)
{
result = canvas.scaleFactor;
}
else if (this.m_FontData.fontSize <= 0 || this.font.fontSize <= 0)
{
result = 1f;
}
else
{
result = (float)this.font.fontSize / (float)this.m_FontData.fontSize;
}
return result;
}
}

Developer Support

unread,
Mar 8, 2017, 2:50:38 PM3/8/17
to Developer Support

Thanks for posting! I don't have access to the Torment project, but this sounds like it is related to the specifics of their canvas setup and is not any sort of general purpose fix. I've attached an image to explain. In all cases in this example image the CanvasScalar is set to "Scale with Screen Size".


The two images on the left side show HyperText (as well as built-in Unity Text) as it currently is, where the pixelsPerUnit are scaled. Along the top, the Canvas Scalar's reference resolution is 4000 x 2250. The one on the right which returns a constant pixelsPerUnit (what this person is suggesting doing) arguably looks more "crisp", but on my HIDPI display is better described as looking "aliased". The reason is because it is oversampling the font size (i.e. picking a font size that is larger than necessary and scaling the geometry down). However, when the reference resolution is smaller than the actual display resolution (400 x 225), you can see that returning a constant pixelsPerUnit value of 1 actually undersamples (i.e. picks a font size smaller than necessary and scales the geometry up). In contrast, using the scaled pixelsPerUnit as is done now ensures the appearance is the same no matter what the settings on the CanvasScalar component.


That said, this post originally suggested that HyperText looks different from the built-in Unity Text under the same conditions, and I still have not received a reproducible bug from anyone for that issue :(

HyperText-Blur.png

baop...@gmail.com

unread,
Mar 8, 2017, 2:53:25 PM3/8/17
to Developer Support
Hi there, thanks for the responses. I've compared HyperText and Unity original Text. They looked the same to me, so I guess no improvement has come from Unity side yet. My apology

Developer Support

unread,
Mar 8, 2017, 2:57:13 PM3/8/17
to Developer Support
Thanks for the follow-up!

mov...@gmail.com

unread,
Mar 8, 2017, 6:40:13 PM3/8/17
to Developer Support
I see. My hypothesis was that the blurry text caused by this issue:

https://issuetracker.unity3d.com/issues/ui-text-is-blurry-with-its-width-and-height-set-to-uneven-numbers

The issue tracker says it's fixed in Unity 5.5, but the earliest reference I've found was in a Unity 5.6 beta.

Developer Support

unread,
Mar 9, 2017, 5:19:28 AM3/9/17
to Developer Support
Thanks for bringing this to my attention! This particular issue is in fact fixed in Unity 5.5.0f3 and newer, both for HyperText and the built-in Text component.
Message has been deleted

mov...@gmail.com

unread,
Mar 10, 2017, 11:48:29 AM3/10/17
to Developer Support
Ah, Torment is built in Unity 5.4.1 and perhaps also using an older version of HyperText.

A different user-made patch doesn't use a constant PPU, but sets the canvas to pixel perfect:

public void fix() {
GameObject ui = GameObject.Find("inXile.UI");
Canvas canvas = ui.GetComponent<Canvas>();
CanvasScaler scaler = ui.GetComponent<CanvasScaler>();

canvas.pixelPerfect = true;
canvas.scaleFactor = (float) Screen.height / 2160f;

scaler.referenceResolution = new Vector2(Screen.width, Screen.height);
scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize;
scaler.scaleFactor = (float) Screen.height / 2160f;
}


However it has some downsides:

- In some cases there will be a visible 1px separation between UI sprites
- A pixel perfect canvas has a significant performance penalty, eg. from 60fps to 40fps in some situations

Any suggestions to improve the user-made patch? Or is it a more complex matter of InXile having to rearrange the UI?

e55cr...@gmail.com

unread,
Mar 12, 2017, 11:42:59 AM3/12/17
to Developer Support
Thank you for the details on pixelsPerUnit.

I don't want to misrepresent my position, I've never developed on Unity or have
any academic understanding of how it renders text. These "fixes" are not optimal
solutions but instead trying something on a hunch and seeing if it produces
better results, because at present Torment renders text poorly.

Linked are three images that display the differences in attempts at fixing the
issue. Torment uses a referenceResolution of 3840x2160 and these are the results
on a 1920x1080 display.

Torment
http://i.imgur.com/dieFhFB.png

pixelsPerUnit as a constant 1.0
http://i.imgur.com/i9xbk5h.png

pixelPerfect which modifies various settings, the code is in previous mail
http://i.imgur.com/kgoIQaa.png

None produce optimal results. Torment is blurry, pixelsPerUnit has a perceived
luminance change if the text is translated as in a moving camera, pixelPerfect
affects all UI elements not just text.

I don't expect inXile to push a patch fixing this in a timely manner, so any
advice you're willing to give would be very appreciated.

Developer Support

unread,
Mar 12, 2017, 1:51:12 PM3/12/17
to Developer Support
Thanks for following up! My point was only that the suggested fix was not something I could apply as a general fix to HyperText, because it would in fact break it.

I'm not in a position to offer support for Torment players—certainly not for unofficial patches to managed code in a shipped game—but I did look into this some more. Unfortunately the complete set of changes necessary for the proper fix to the bug linked by movrajr includes changes to native Unity code, so the only real options for inXile are to either a) upgrade to Unity 5.5.x or b) graft in the changeset(s) with the necessary fixes (assuming they have Unity source access). Of course both of these changes assume that the issue linked is the actual source of the problem.

Developer Support

unread,
Mar 12, 2017, 4:51:30 PM3/12/17
to Developer Support
Hi again! I looked a bit more and actually managed to find a fix for the linked issue I could implement on the managed side that works all the way down to Unity 4.7.0. (So bonus for anyone using HyperText on old versions of Unity is it should look better than built-in Text now.) I've pinged the appropriate people at inXile, so hopefully it's easy enough for them to drop in the newest version (1.8.15 on the Asset Store now) and see if it helps.

If you're feeling adventurous hacking the Torment assemblies, you can try it out and see if it gives you better results than messing with the pixelsPerUnit (or enabling pixel perfect). Note that it is the fix for the public issue linked by movrajr, which makes Text (and HyperText) appear blurry if the RectTransform has an odd value on either its width or height dimension, so it's entirely possible it may be unrelated. Basically, there is an OnPopulateMesh method, and part of it looks something like this (in C#):

Vector2 textAnchorPivot = GetTextAnchorPivot(this.alignment);
Vector2 refPoint = Vector2.zero;
refPoint.x = Mathf.Lerp(inputRect.xMin, inputRect.xMax, textAnchorPivot.x);
refPoint.y = Mathf.Lerp(inputRect.yMin, inputRect.yMax, textAnchorPivot.y);
Vector2 roundingOffset = PixelAdjustPoint(refPoint) - refPoint;

It instead needs to look like this:

Vector2 refPoint = new Vector2(m_UIVertices[0].position.x, m_UIVertices[0].position.y) * unitsPerPixel;
Vector2 pixelAdjustPoint = PixelAdjustPoint(refPoint);
pixelAdjustPoint.x = Mathf.FloorToInt(pixelAdjustPoint.x);
pixelAdjustPoint.y = Mathf.FloorToInt(pixelAdjustPoint.y);
Vector2 roundingOffset = pixelAdjustPoint - refPoint;

mov...@gmail.com

unread,
Mar 12, 2017, 7:47:18 PM3/12/17
to Developer Support
Wow, thanks for digging into the issue. Much appreciated.

You are right, whether issue 826409 is indeed the culprit is a stab in the dark. But we will try your suggested fix and perhaps it will do the trick. :)

e55cr...@gmail.com

unread,
Mar 17, 2017, 4:50:57 PM3/17/17
to Developer Support
Sorry for the delayed follow up, OnPopulateMesh is a complicated method and it
proved difficult to produce a valid assembly after modifications.

Unfortunately this change appears to be a slight regression for Torment. I may
have applied it incorrectly, perhaps you could point out any errors I made. Do
note that this was sourced from disassembling the assembly that contains
Candlelight.UI, it might not appear exactly as you guys wrote it.

Original
https://paste.pound-python.org/show/faiLlVtY0BXXr8BFQHu5/
Modified
https://paste.pound-python.org/show/c2eBURenK0ynShmRJVtQ/

Original
http://i.imgur.com/dieFhFB.png
Modified
http://i.imgur.com/PWJcOx3.png

It's subtle but it looks like it moved the text down half a pixel and produces
very slightly worse text. I logged the values to get an idea of what it's
doing, an excerpt here with the first four vertices that if I understand
correctly is the first glyph "C".

Continue
refPoint: (-25.5, 5.0)
pixelAdjustPoint: (-26.0, 5.0)
roundingOffset: (-0.5, 0.0)
scale: 2
// from is before applying scale and roundingOffset
// to is after
pos from: (-51.0, 10.0, 0.0) to: (-102.5, 20.0, 0.0)
pos from: (-35.0, 10.0, 0.0) to: (-70.5, 20.0, 0.0)
pos from: (-35.0, -9.0, 0.0) to: (-70.5, -18.0, 0.0)
pos from: (-51.0, -9.0, 0.0) to: (-102.5, -18.0, 0.0)

A full dump of the main menu if it's of value to you
https://paste.pound-python.org/show/BSNUymftnxxhwWrXZPaC/

I think maybe I was supposed to apply the scale after adjusting for the
rounding offset, but then it's not clear to me what happens when scale is not
an even integer. Of note is that this appears to be local coordinates in
relation to the center of some containing object and this leads me to believe
that somewhere up the scene graph is where the issue arises.

There are components like ContentSizeFitter and LayoutElement which could be
taking our nice aligned vertices and then introducing fractions. I'd like to
log these vertices after transforms from the layout stuff, and also after the
camera transform is applied but I don't know how to do that. There's something
like LocalToWorldUnits but does it take the layout into account? What about
camera?

A snippet of the scene graph where GameObjects are denoted with +:
https://paste.pound-python.org/show/J61wXwWfCRaUQUoUjod3/


This mail was longer than anticipated, I just want to say thanks for looking
into the issue thus far and it's not lost on me that you've provided support
for someone who's not even a customer.

mov...@gmail.com

unread,
Mar 17, 2017, 5:00:49 PM3/17/17
to Developer Support
The first line of the suggested fix was:

Vector2 refPoint = new Vector2(m_UIVertices[0].position.x, m_UIVertices[0].position.y) * unitsPerPixel;

Note it says unitsPerPixel instead of pixelsPerUnit.

I changed the code as follows in HyperText.OnPopulateMesh:

float d = 1f / this.pixelsPerUnit;
Vector2 zero = new Vector2(m_UIVertices[0].position.x, m_UIVertices[0].position.y) * d;
Vector2 pixelAdjustPoint = PixelAdjustPoint(zero);


pixelAdjustPoint.x = Mathf.FloorToInt(pixelAdjustPoint.x);
pixelAdjustPoint.y = Mathf.FloorToInt(pixelAdjustPoint.y);

Vector2 lhs = pixelAdjustPoint - zero;

However I've not been to test it in-game because the tool I'm using throws errors when I try to modify the assembly.

e55cr...@gmail.com

unread,
Mar 17, 2017, 7:46:36 PM3/17/17
to Developer Support
On Friday, 17 March 2017 17:00:49 UTC-4, mov...@gmail.com wrote:
> Note it says unitsPerPixel instead of pixelsPerUnit.

Good catch. After correcting this I've been unable to find an instance where
Torment and this change differs, although I have logged that some text
components do indeed have odd or even fractional widths and heights.

This led me to something, the texts are formatted strings - for example:
(color="#77BAF1FF")Callistege - (/color)(color="#B5B5B5FF")(color="#FFFFFFFF")"Watch your steps, child. This is not a place to walk lightly. There are dangers in the Reef, both old and new."(/color)(/color)

Angle brackets were converted to round brackets in case Groups would interpret
that as formatted text. That's 206 characters total, and m_UIVertices has 824
items (after removing 4 which get truncated later) which can be broken down as
206 * 4 verts per glyph = 824. Alternatively if we were clever; we wouldn't
generate vertices for spaces and neither would we generate vertices for the
xml-like formatting. 11 characters in `Callistege-` and 91 in `"Watchyour..."`
if there's actually 2 quads per char for a text shadow or something, then
(11 + 91) * 2 * 4 = 816. Off by 8, which would be one char unaccounted for
before multiplying by 2 for a shadow or border or some such. Maybe it's that
line break that's unaccounted for. That's a bit of a coincidence, could you
shed some light on how we get 824 verts for 125 printable chars? Torment has
a performance issue when scrolling large bodies of text, and maybe that can be
mitigated by eliminating unneeded vertices.

The log of that text component is
rect: (x:0.00, y:-98.00, width:1845.00, height:98.00)
refPoint: (0.0, -38.0)
pixelAdjustPoint: (0.0, -38.0)
roundingOffset: (0.0, 0.0)
scale: 2

When we divide the width by 2 to scale from referenceResolution to my
resolution, we end with a width of 922.5 although the verts generated are
whole integers. It might just be that the text ends up on world coordinate
x:something.5, or some other fraction, because of the sizers and layout system
and this causes blurry text.

Developer Support

unread,
Mar 18, 2017, 6:59:39 AM3/18/17
to Developer Support
Hi there! Lots of stuff came up here, but to be brief:

1. I forgot the second part of the blurriness fix is the HyperText.preferredHeight property. What does it look like for you? I'm not sure what version Torment is using, but older versions of HyperText had:

UpdateTextProcessor();
return this.cachedTextGeneratorForLayout.GetPreferredHeight(
    this.TextProcessor.OutputText,
    GetGenerationSettings(new Vector2(this.RectTransform.rect.size.x, 0f))
) / this.pixelsPerUnit;
  

It needs to look like this (in C#):

UpdateTextProcessor();
return this.cachedTextGeneratorForLayout.GetPreferredHeight(
    this.TextProcessor.OutputText,
    GetGenerationSettings(new Vector2(GetPixelAdjustedRect().size.x, 0f))
) / this.pixelsPerUnit;
  

Curious to know if this changes anything!

2. HyperText uses Unity's built-in TextGenerator, which as you noticed creates degenerate vertices for spaces and rich text tags. While it has its annoyances, this should be unrelated to any scrolling issues. My suspicion where that is concerned is that Torment (as any text-heavy game in Unity must) implements some kind of custom solution for pooling Text/HyperText instances and cycling through them as you scroll. Whenever this happens, new text data gets uploaded to the HyperText instances, which have to rebuild geometry. One of the biggest performance hits here comes from accessors in Unity's Mesh APIs, which allocate new arrays with every read. Unity 5.5.2 and newer have non-allocating API points, which HyperText uses, but Torment would have to be updated to a newer version of Unity to take advantage of them.

e55cr...@gmail.com

unread,
Mar 18, 2017, 10:41:52 AM3/18/17
to Developer Support
preferredHeight looks the same with the exception of `RectTransform` being
`rectTransform`, I assume this to be a typo. Only HyperText.Quad has a
`RectTransform` property, not HyperText.

Here's the change I made, which will log the differences between the original,
your proposed change, and another one I tried. GetPixelAdjustedRect() just
returns rectTransform.rect when cavas.pixelPerfect is false, so the third one
is the rect we'd get if pixelPerfect was true for the scope of
GetPixelAdjustedRect(). Explicit base and this for unambiguity as the IL needed
is different and the outcomes as well, specifically for GetGenerationSettings
which caused incorrect results.


public override float preferredHeight {
get {
this.UpdateTextProcessor();

/* Original */
Rect r1 = base.rectTransform.rect;
float f1 = base.cachedTextGeneratorForLayout.GetPreferredHeight(
this.TextProcessor.OutputText,
this.GetGenerationSettings(new Vector2(r1.size.x, 0f))
) / this.pixelsPerUnit;

/* Modified */
Rect r2 = this.GetPixelAdjustedRect();
float f2 = base.cachedTextGeneratorForLayout.GetPreferredHeight(
this.TextProcessor.OutputText,
this.GetGenerationSettings(new Vector2(r2.size.x, 0f))
) / this.pixelsPerUnit;

/* Modified 2 */
Rect r3 = RectTransformUtility.PixelAdjustRect(base.rectTransform, base.canvas);
float f3 = base.cachedTextGeneratorForLayout.GetPreferredHeight(
this.TextProcessor.OutputText,
this.GetGenerationSettings(new Vector2(r3.size.x, 0f))
) / this.pixelsPerUnit;

Debug.Log(this.TextProcessor.OutputText);
Debug.Log(r1.ToString() + " :: " + f1.ToString());
Debug.Log(r2.ToString() + " :: " + f2.ToString());
Debug.Log(r3.ToString() + " :: " + f3.ToString());

return f2;
}
}


Callistege // this is a floating name tag and not part of previous screenshots
(x:0.00, y:-25.00, width:700.00, height:50.00) :: 50
(x:0.00, y:-25.00, width:700.00, height:50.00) :: 50
(x:0.15, y:-25.30, width:700.00, height:50.00) :: 50
<color="#7888F3FF">1. "What kind of dangers are there in the Reef?"</color>
(x:0.00, y:-100.00, width:1725.00, height:100.00) :: 50
(x:0.00, y:-100.00, width:1725.00, height:100.00) :: 50
(x:-1.00, y:-100.00, width:1726.00, height:100.00) :: 50


No changes in the returned height in the logs. Checked visually just to be
certain, no discernible effect.

e55cr...@gmail.com

unread,
Mar 20, 2017, 12:04:38 PM3/20/17
to Developer Support
I've been able to make the text clear without modifying the canvas, though
it's from the outside-in rather than changing something in HyperText.

The problem appears to be text objects ending up on non-integer screen pos,
and truncating their final calculated position is sufficient to get good
looking text. I don't know what changes would need to be made to get HyperText
to ensure it lands on an integer position, sorry. I bound a hotkey that runs
this function:

foreach(HyperText ht in GameObject.FindObjectsOfType<HyperText>()) {
Camera camera = ht.canvas.worldCamera;

Vector3 screenpos = camera.WorldToScreenPoint(
ht.gameObject.transform.position
);

ht.gameObject.transform.position = camera.ScreenToWorldPoint(
new Vector3(
Mathf.FloorToInt(screenpos.x),
Mathf.FloorToInt(screenpos.y),
Mathf.FloorToInt(screenpos.z)
)
);
}

Various screenshots that show before and after
http://i.imgur.com/AKfCM8y.png http://i.imgur.com/bwyrO9I.png
http://i.imgur.com/NnL8y3o.png http://i.imgur.com/UZq0VJ7.png
http://i.imgur.com/OvROnyh.png http://i.imgur.com/Q4UyaLA.png
http://i.imgur.com/l7JjTrR.png http://i.imgur.com/ECpAdzT.png

It does a great job the majority of the time, although there are some holdouts
that are just a tiny bit off. These holdouts are from floating point precision
errors, as there is no perfect mapping from screen position to world coord.

I noticed Torment has some really bizarre coordinate scale, in the main menu
y coord 5.0 is the bottom of my screen and 10.0 is the top. Presumably y=0.0
is the bottom of the screen on a 3840x2160 display, and y=15.0 would be top.
There's no clear relationship there that I can see for 16 units being 2160
pixels.

Having to hit a hotkey every time new text appears or layout gets recalculated
is obviously not ideal, any tips for which function to apply this in to do it
automatically and efficiently?

Developer Support

unread,
Mar 21, 2017, 3:42:45 AM3/21/17
to Developer Support
OK sure I have a repro now, where I can slightly adjust the canvas scale (by e.g., changing the weighting between width/height on a canvas that is set to scale with screen size) and see that although the text output is the same, slight adjustments in geometry placement/scale cause the text to appear blurry sometimes but not others. The problem no longer exists in Unity 5.5.0 and higher, so I can take a quick glance to see if I can figure out what made the difference and if it is possible to implement a similar fix in managed code. That said, but because it is no longer a problem in 5.5.0, I can only dig so deep before I can't really invest more time in the issue.

Developer Support

unread,
Mar 22, 2017, 5:57:43 PM3/22/17
to Developer Support
Ok I think I have something you can try, as it fixes the repro case I was able to come up with. There should be a part that looks something like this:

this.cachedTextGenerator.Populate(PostprocessText(), GetGenerationSettings(inputRect.size));
this.cachedTextGenerator.GetVertices(m_UIVertices);
Vector2 refPoint = new Vector2(m_UIVertices[0].position.x, m_UIVertices[0].position.y) * unitsPerPixel;
Vector2 pixelAdjustPoint = PixelAdjustPoint(refPoint);
pixelAdjustPoint.x = Mathf.FloorToInt(pixelAdjustPoint.x);
pixelAdjustPoint.y = Mathf.FloorToInt(pixelAdjustPoint.y);
Vector2 roundingOffset = pixelAdjustPoint - refPoint;

It should instead look like this:

this.cachedTextGenerator.Populate(PostprocessText(), GetGenerationSettings(inputRect.size));
this.cachedTextGenerator.GetVertices(m_UIVertices);
Vector2 refPoint = new Vector2(m_UIVertices[0].position.x, m_UIVertices[0].position.y) * unitsPerPixel;
Vector2 pixelAdjustPoint = PixelAdjustPoint(refPoint);
pixelAdjustPoint *= pixelsPerUnit; // Add this
pixelAdjustPoint.x = Mathf.FloorToInt(pixelAdjustPoint.x);
pixelAdjustPoint.y = Mathf.FloorToInt(pixelAdjustPoint.y);
pixelAdjustPoint *= unitsPerPixel; // Add this

mov...@gmail.com

unread,
Mar 28, 2017, 7:39:51 AM3/28/17
to Developer Support
@Crashed, did you have a chance to implement the developer's suggested fix?
Reply all
Reply to author
Forward
0 new messages