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.
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;
}
}
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 :(
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.
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?
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.
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. :)
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.
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.
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.
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.
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?