Selaa lähdekoodia

Adding Text3D to Graphics module

Josh Engebretson 8 vuotta sitten
vanhempi
sitoutus
539d759f09

+ 1 - 1
Source/Atomic/CMakeLists.txt

@@ -20,7 +20,7 @@ file (GLOB SYSTEM_UI_SOURCE UI/SystemUI/*.cpp UI/SystemUI/*.h)
 file (GLOB PHYSICS_SOURCE Physics/*.cpp Physics/*.h)
 file (GLOB NAVIGATION_SOURCE Navigation/*.cpp Navigation/*.h)
 file (GLOB ENVIRONMENT_SOURCE Environment/*.cpp Environment/*.h)
-file (GLOB GRAPHICS_SOURCE Graphics/*.cpp Graphics/*.h)
+file (GLOB GRAPHICS_SOURCE Graphics/*.cpp Graphics/*.h Graphics/Text3D/*.cpp Graphics/Text3D/*.h)
 if (ATOMIC_IK)
     file (GLOB IK_SOURCE IK/*.cpp IK/*.h)
 endif ()

+ 11 - 0
Source/Atomic/Graphics/Graphics.cpp

@@ -53,6 +53,11 @@
 #include "../IO/Log.h"
 
 // ATOMIC BEGIN
+
+#include "Text3D/Text3DFont.h"
+#include "Text3D/Text3DText.h"
+#include "Text3D/Text3D.h"
+
 #include <SDL/include/SDL.h>
 #include <SDL/include/SDL_syswm.h>
 // ATOMIC END
@@ -404,6 +409,12 @@ void RegisterGraphicsLibrary(Context* context)
     DebugRenderer::RegisterObject(context);
     Octree::RegisterObject(context);
     Zone::RegisterObject(context);
+
+    // ATOMIC BEGIN
+    Text3DFont::RegisterObject(context);
+    Text3DText::RegisterObject(context);
+    Text3D::RegisterObject(context);
+    // ATOMIC END
 }
 
 // ATOMIC BEGIN

+ 760 - 0
Source/Atomic/Graphics/Text3D/Text3D.cpp

@@ -0,0 +1,760 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../../Core/Context.h"
+#include "../Camera.h"
+#include "../Geometry.h"
+#include "../Graphics.h"
+#include "../Material.h"
+#include "../Technique.h"
+#include "../VertexBuffer.h"
+#include "../../IO/Log.h"
+#include "../../Resource/ResourceCache.h"
+#include "../../Scene/Node.h"
+#include "Text3DFont.h"
+#include "Text3DText.h"
+#include "Text3D.h"
+
+namespace Atomic
+{
+
+extern const char* horizontalAlignments[];
+extern const char* verticalAlignments[];
+extern const char* textEffects[];
+extern const char* faceCameraModeNames[];
+extern const char* GEOMETRY_CATEGORY;
+
+static const float TEXT_SCALING = 1.0f / 128.0f;
+static const float DEFAULT_EFFECT_DEPTH_BIAS = 0.1f;
+
+Text3D::Text3D(Context* context) :
+    Drawable(context, DRAWABLE_GEOMETRY),
+    text_(context),
+    vertexBuffer_(new VertexBuffer(context_)),
+    customWorldTransform_(Matrix3x4::IDENTITY),
+    faceCameraMode_(FC_NONE),
+    minAngle_(0.0f),
+    fixedScreenSize_(false),
+    textDirty_(true),
+    geometryDirty_(true),
+    usingSDFShader_(false),
+    fontDataLost_(false),
+    horizontalAlignment_(HA_CENTER),
+    verticalAlignment_(VA_CENTER)
+{
+    text_.SetEffectDepthBias(DEFAULT_EFFECT_DEPTH_BIAS);
+}
+
+Text3D::~Text3D()
+{
+}
+
+void Text3D::RegisterObject(Context* context)
+{
+    context->RegisterFactory<Text3D>(GEOMETRY_CATEGORY);
+
+    ATOMIC_ACCESSOR_ATTRIBUTE("Is Enabled", IsEnabled, SetEnabled, bool, true, AM_DEFAULT);
+    ATOMIC_MIXED_ACCESSOR_ATTRIBUTE("Font", GetFontAttr, SetFontAttr, ResourceRef, ResourceRef(Text3DFont::GetTypeStatic()), AM_DEFAULT);
+    ATOMIC_MIXED_ACCESSOR_ATTRIBUTE("Material", GetMaterialAttr, SetMaterialAttr, ResourceRef, ResourceRef(Material::GetTypeStatic()),
+        AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Font Size", int, text_.fontSize_, TEXT3D_DEFAULT_FONT_SIZE, AM_DEFAULT);
+    ATOMIC_MIXED_ACCESSOR_ATTRIBUTE("Text", GetTextAttr, SetTextAttr, String, String::EMPTY, AM_DEFAULT);
+    ATOMIC_ENUM_ATTRIBUTE("Text Alignment", text_.textAlignment_, horizontalAlignments, HA_LEFT, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Row Spacing", float, text_.rowSpacing_, 1.0f, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Word Wrap", bool, text_.wordWrap_, false, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Can Be Occluded", IsOccludee, SetOccludee, bool, true, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Fixed Screen Size", IsFixedScreenSize, SetFixedScreenSize, bool, false, AM_DEFAULT);
+    ATOMIC_ENUM_ATTRIBUTE("Face Camera Mode", faceCameraMode_, faceCameraModeNames, FC_NONE, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Min Angle", float, minAngle_, 0.0f, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Draw Distance", GetDrawDistance, SetDrawDistance, float, 0.0f, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Width", GetWidth, SetWidth, int, 0, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Opacity", GetOpacity, SetOpacity, float, 1.0f, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Color", GetColorAttr, SetColor, Color, Color::WHITE, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Top Left Color", Color, text_.color_[0], Color::WHITE, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Top Right Color", Color, text_.color_[1], Color::WHITE, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Bottom Left Color", Color, text_.color_[2], Color::WHITE, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Bottom Right Color", Color, text_.color_[3], Color::WHITE, AM_DEFAULT);
+    ATOMIC_ENUM_ATTRIBUTE("Text Effect", text_.textEffect_, textEffects, TE_NONE, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Shadow Offset", IntVector2, text_.shadowOffset_, IntVector2(1, 1), AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Stroke Thickness", int, text_.strokeThickness_, 1, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Round Stroke", bool, text_.roundStroke_, false, AM_DEFAULT);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Effect Color", GetEffectColor, SetEffectColor, Color, Color::BLACK, AM_DEFAULT);
+    ATOMIC_ATTRIBUTE("Effect Depth Bias", float, text_.effectDepthBias_, DEFAULT_EFFECT_DEPTH_BIAS, AM_DEFAULT);
+    ATOMIC_COPY_BASE_ATTRIBUTES(Drawable);
+}
+
+void Text3D::ApplyAttributes()
+{
+    text_.ApplyAttributes();
+    MarkTextDirty();
+    UpdateTextBatches();
+    UpdateTextMaterials();
+}
+
+void Text3D::UpdateBatches(const FrameInfo& frame)
+{
+    distance_ = frame.camera_->GetDistance(GetWorldBoundingBox().Center());
+
+    if (faceCameraMode_ != FC_NONE || fixedScreenSize_)
+        CalculateFixedScreenSize(frame);
+
+    for (unsigned i = 0; i < batches_.Size(); ++i)
+    {
+        batches_[i].distance_ = distance_;
+        batches_[i].worldTransform_ = faceCameraMode_ != FC_NONE ? &customWorldTransform_ : &node_->GetWorldTransform();
+    }
+
+    for (unsigned i = 0; i < uiBatches_.Size(); ++i)
+    {
+        if (uiBatches_[i].texture_ && uiBatches_[i].texture_->IsDataLost())
+        {
+            fontDataLost_ = true;
+            break;
+        }
+    }
+}
+
+void Text3D::UpdateGeometry(const FrameInfo& frame)
+{
+    if (fontDataLost_)
+    {
+        // Re-evaluation of the text triggers the font face to reload itself
+        UpdateTextBatches();
+        UpdateTextMaterials();
+        fontDataLost_ = false;
+    }
+
+    // In case is being rendered from multiple views, recalculate camera facing & fixed size
+    if (faceCameraMode_ != FC_NONE || fixedScreenSize_)
+        CalculateFixedScreenSize(frame);
+
+    if (geometryDirty_)
+    {
+        for (unsigned i = 0; i < batches_.Size() && i < uiBatches_.Size(); ++i)
+        {
+            Geometry* geometry = geometries_[i];
+            geometry->SetDrawRange(TRIANGLE_LIST, 0, 0, uiBatches_[i].vertexStart_ / TEXT3D_VERTEX_SIZE,
+                (uiBatches_[i].vertexEnd_ - uiBatches_[i].vertexStart_) / TEXT3D_VERTEX_SIZE);
+        }
+    }
+
+    if ((geometryDirty_ || vertexBuffer_->IsDataLost()) && uiVertexData_.Size())
+    {
+        unsigned vertexCount = uiVertexData_.Size() / TEXT3D_VERTEX_SIZE;
+        if (vertexBuffer_->GetVertexCount() != vertexCount)
+            vertexBuffer_->SetSize(vertexCount, MASK_POSITION | MASK_COLOR | MASK_TEXCOORD1);
+        vertexBuffer_->SetData(&uiVertexData_[0]);
+    }
+
+    geometryDirty_ = false;
+}
+
+UpdateGeometryType Text3D::GetUpdateGeometryType()
+{
+    if (geometryDirty_ || fontDataLost_ || vertexBuffer_->IsDataLost() || faceCameraMode_ != FC_NONE || fixedScreenSize_)
+        return UPDATE_MAIN_THREAD;
+    else
+        return UPDATE_NONE;
+}
+
+void Text3D::SetMaterial(Material* material)
+{
+    material_ = material;
+
+    UpdateTextMaterials(true);
+}
+
+bool Text3D::SetFont(const String& fontName, int size)
+{
+    bool success = text_.SetFont(fontName, size);
+
+    // Changing font requires materials to be re-evaluated. Material evaluation can not be done in worker threads,
+    // so UI batches must be brought up-to-date immediately
+    MarkTextDirty();
+    UpdateTextBatches();
+    UpdateTextMaterials();
+
+    return success;
+}
+
+bool Text3D::SetFont(Text3DFont* font, int size)
+{
+    bool success = text_.SetFont(font, size);
+
+    MarkTextDirty();
+    UpdateTextBatches();
+    UpdateTextMaterials();
+
+    return success;
+}
+
+bool Text3D::SetFontSize(int size)
+{
+    bool success = text_.SetFontSize(size);
+
+    MarkTextDirty();
+    UpdateTextBatches();
+    UpdateTextMaterials();
+
+    return success;
+}
+
+void Text3D::SetText(const String& text)
+{
+    text_.SetText(text);
+
+    // Changing text requires materials to be re-evaluated, in case the font is multi-page
+    MarkTextDirty();
+    UpdateTextBatches();
+    UpdateTextMaterials();
+}
+
+void Text3D::SetTextAlignment(Text3DHorizontalAlignment align)
+{
+    text_.SetTextAlignment(align);
+
+    MarkTextDirty();
+}
+
+void Text3D::SetRowSpacing(float spacing)
+{
+    text_.SetRowSpacing(spacing);
+
+    MarkTextDirty();
+}
+
+void Text3D::SetWordwrap(bool enable)
+{
+    text_.SetWordwrap(enable);
+
+    MarkTextDirty();
+}
+
+void Text3D::SetTextEffect(Text3DTextEffect textEffect)
+{
+    text_.SetTextEffect(textEffect);
+
+    MarkTextDirty();
+    UpdateTextMaterials(true);
+}
+
+void Text3D::SetEffectShadowOffset(const IntVector2& offset)
+{
+    text_.SetEffectShadowOffset(offset);
+}
+
+void Text3D::SetEffectStrokeThickness(int thickness)
+{
+    text_.SetEffectStrokeThickness(thickness);
+}
+
+void Text3D::SetEffectRoundStroke(bool roundStroke)
+{
+    text_.SetEffectRoundStroke(roundStroke);
+}
+
+void Text3D::SetEffectColor(const Color& effectColor)
+{
+    text_.SetEffectColor(effectColor);
+
+    MarkTextDirty();
+    UpdateTextMaterials();
+}
+
+void Text3D::SetEffectDepthBias(float bias)
+{
+    text_.SetEffectDepthBias(bias);
+
+    MarkTextDirty();
+}
+
+void Text3D::SetWidth(int width)
+{
+    text_.SetMinWidth(width);
+    text_.SetWidth(width);
+
+    MarkTextDirty();
+}
+
+void Text3D::SetColor(const Color& color)
+{
+    float oldAlpha = text_.GetColor(C_TOPLEFT).a_;
+    text_.SetColor(color);
+
+    MarkTextDirty();
+
+    // If alpha changes from zero to nonzero or vice versa, amount of text batches changes (optimization), so do full update
+    if ((oldAlpha == 0.0f && color.a_ != 0.0f) || (oldAlpha != 0.0f && color.a_ == 0.0f))
+    {
+        UpdateTextBatches();
+        UpdateTextMaterials();
+    }
+}
+
+void Text3D::SetColor(Text3DCorner corner, const Color& color)
+{
+    text_.SetColor(corner, color);
+
+    MarkTextDirty();
+}
+
+void Text3D::SetOpacity(float opacity)
+{
+    float oldOpacity = text_.GetOpacity();
+    text_.SetOpacity(opacity);
+    float newOpacity = text_.GetOpacity();
+
+    MarkTextDirty();
+
+    // If opacity changes from zero to nonzero or vice versa, amount of text batches changes (optimization), so do full update
+    if ((oldOpacity == 0.0f && newOpacity != 0.0f) || (oldOpacity != 0.0f && newOpacity == 0.0f))
+    {
+        UpdateTextBatches();
+        UpdateTextMaterials();
+    }
+}
+
+void Text3D::SetFixedScreenSize(bool enable)
+{
+    if (enable != fixedScreenSize_)
+    {
+        fixedScreenSize_ = enable;
+
+        // Bounding box must be recalculated
+        OnMarkedDirty(node_);
+        MarkNetworkUpdate();
+    }
+}
+
+void Text3D::SetFaceCameraMode(FaceCameraMode mode)
+{
+    if (mode != faceCameraMode_)
+    {
+        faceCameraMode_ = mode;
+
+        // Bounding box must be recalculated
+        OnMarkedDirty(node_);
+        MarkNetworkUpdate();
+    }
+}
+
+Material* Text3D::GetMaterial() const
+{
+    return material_;
+}
+
+Text3DFont* Text3D::GetFont() const
+{
+    return text_.GetFont();
+}
+
+int Text3D::GetFontSize() const
+{
+    return text_.GetFontSize();
+}
+
+const String& Text3D::GetText() const
+{
+    return text_.GetText();
+}
+
+Text3DHorizontalAlignment Text3D::GetTextAlignment() const
+{
+    return text_.GetTextAlignment();
+}
+
+float Text3D::GetRowSpacing() const
+{
+    return text_.GetRowSpacing();
+}
+
+bool Text3D::GetWordwrap() const
+{
+    return text_.GetWordwrap();
+}
+
+Text3DTextEffect Text3D::GetTextEffect() const
+{
+    return text_.GetTextEffect();
+}
+
+const IntVector2& Text3D::GetEffectShadowOffset() const
+{
+    return text_.GetEffectShadowOffset();
+}
+
+int Text3D::GetEffectStrokeThickness() const
+{
+    return text_.GetEffectStrokeThickness();
+}
+
+bool Text3D::GetEffectRoundStroke() const
+{
+    return text_.GetEffectRoundStroke();
+}
+
+const Color& Text3D::GetEffectColor() const
+{
+    return text_.GetEffectColor();
+}
+
+float Text3D::GetEffectDepthBias() const
+{
+    return text_.GetEffectDepthBias();
+}
+
+int Text3D::GetWidth() const
+{
+    return text_.GetWidth();
+}
+
+int Text3D::GetHeight() const
+{
+    return text_.GetHeight();
+}
+
+int Text3D::GetRowHeight() const
+{
+    return text_.GetRowHeight();
+}
+
+unsigned Text3D::GetNumRows() const
+{
+    return text_.GetNumRows();
+}
+
+unsigned Text3D::GetNumChars() const
+{
+    return text_.GetNumChars();
+}
+
+int Text3D::GetRowWidth(unsigned index) const
+{
+    return text_.GetRowWidth(index);
+}
+
+IntVector2 Text3D::GetCharPosition(unsigned index)
+{
+    return text_.GetCharPosition(index);
+}
+
+IntVector2 Text3D::GetCharSize(unsigned index)
+{
+    return text_.GetCharSize(index);
+}
+
+const Color& Text3D::GetColor(Text3DCorner corner) const
+{
+    return text_.GetColor(corner);
+}
+
+float Text3D::GetOpacity() const
+{
+    return text_.GetOpacity();
+}
+
+void Text3D::OnNodeSet(Node* node)
+{
+    Drawable::OnNodeSet(node);
+
+    if (node)
+        customWorldTransform_ = node->GetWorldTransform();
+}
+
+void Text3D::OnWorldBoundingBoxUpdate()
+{
+    if (textDirty_)
+        UpdateTextBatches();
+
+    // In face camera mode, use the last camera rotation to build the world bounding box
+    if (faceCameraMode_ != FC_NONE || fixedScreenSize_)
+    {
+        worldBoundingBox_ = boundingBox_.Transformed(Matrix3x4(node_->GetWorldPosition(),
+            customWorldTransform_.Rotation(), customWorldTransform_.Scale()));
+    }
+    else
+        worldBoundingBox_ = boundingBox_.Transformed(node_->GetWorldTransform());
+}
+
+void Text3D::MarkTextDirty()
+{
+    textDirty_ = true;
+
+    OnMarkedDirty(node_);
+    MarkNetworkUpdate();
+}
+
+void Text3D::SetMaterialAttr(const ResourceRef& value)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    SetMaterial(cache->GetResource<Material>(value.name_));
+}
+
+void Text3D::SetFontAttr(const ResourceRef& value)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    text_.font_ = cache->GetResource<Text3DFont>(value.name_);
+}
+
+void Text3D::SetTextAttr(const String& value)
+{
+    text_.SetTextAttr(value);
+}
+
+String Text3D::GetTextAttr() const
+{
+    return text_.GetTextAttr();
+}
+
+ResourceRef Text3D::GetMaterialAttr() const
+{
+    return GetResourceRef(material_, Material::GetTypeStatic());
+}
+
+ResourceRef Text3D::GetFontAttr() const
+{
+    return GetResourceRef(text_.font_, Text3DFont::GetTypeStatic());
+}
+
+void Text3D::UpdateTextBatches()
+{
+    uiBatches_.Clear();
+    uiVertexData_.Clear();
+
+    text_.GetBatches(uiBatches_, uiVertexData_, IntRect::ZERO);
+
+    Vector3 offset(Vector3::ZERO);
+
+    switch (GetHorizontalAlignment())
+    {
+    case HA_LEFT:
+        break;
+
+    case HA_CENTER:
+        offset.x_ -= (float)text_.GetWidth() * 0.5f;
+        break;
+
+    case HA_RIGHT:
+        offset.x_ -= (float)text_.GetWidth();
+        break;
+    }
+
+    switch (GetVerticalAlignment())
+    {
+    case VA_TOP:
+        break;
+
+    case VA_CENTER:
+        offset.y_ -= (float)text_.GetHeight() * 0.5f;
+        break;
+
+    case VA_BOTTOM:
+        offset.y_ -= (float)text_.GetHeight();
+        break;
+    }
+
+    if (uiVertexData_.Size())
+    {
+        boundingBox_.Clear();
+
+        for (unsigned i = 0; i < uiVertexData_.Size(); i += TEXT3D_VERTEX_SIZE)
+        {
+            Vector3& position = *(reinterpret_cast<Vector3*>(&uiVertexData_[i]));
+            position += offset;
+            position *= TEXT_SCALING;
+            position.y_ = -position.y_;
+            boundingBox_.Merge(position);
+        }
+    }
+    else
+        boundingBox_.Define(Vector3::ZERO, Vector3::ZERO);
+
+    textDirty_ = false;
+    geometryDirty_ = true;
+}
+
+void Text3D::UpdateTextMaterials(bool forceUpdate)
+{
+    Text3DFont* font = GetFont();
+    bool isSDFFont = font ? font->IsSDFFont() : false;
+
+    batches_.Resize(uiBatches_.Size());
+    geometries_.Resize(uiBatches_.Size());
+
+    for (unsigned i = 0; i < batches_.Size(); ++i)
+    {
+        if (!geometries_[i])
+        {
+            Geometry* geometry = new Geometry(context_);
+            geometry->SetVertexBuffer(0, vertexBuffer_);
+            batches_[i].geometry_ = geometries_[i] = geometry;
+        }
+
+        if (!batches_[i].material_ || forceUpdate || isSDFFont != usingSDFShader_)
+        {
+            // If material not defined, create a reasonable default from scratch
+            if (!material_)
+            {
+                Material* material = new Material(context_);
+                Technique* tech = new Technique(context_);
+                Pass* pass = tech->CreatePass("alpha");
+                pass->SetVertexShader("Text");
+                pass->SetPixelShader("Text");
+                pass->SetBlendMode(BLEND_ALPHA);
+                pass->SetDepthWrite(false);
+                material->SetTechnique(0, tech);
+                material->SetCullMode(CULL_NONE);
+                batches_[i].material_ = material;
+            }
+            else
+                batches_[i].material_ = material_->Clone();
+
+            usingSDFShader_ = isSDFFont;
+        }
+
+        Material* material = batches_[i].material_;
+        Texture* texture = uiBatches_[i].texture_;
+        material->SetTexture(TU_DIFFUSE, texture);
+
+        if (isSDFFont)
+        {
+            // Note: custom defined material is assumed to have right shader defines; they aren't modified here
+            if (!material_)
+            {
+                Technique* tech = material->GetTechnique(0);
+                Pass* pass = tech ? tech->GetPass("alpha") : (Pass*)0;
+                if (pass)
+                {
+                    switch (GetTextEffect())
+                    {
+                    case TE_NONE:
+                        pass->SetPixelShaderDefines("SIGNED_DISTANCE_FIELD");
+                        break;
+
+                    case TE_SHADOW:
+                        pass->SetPixelShaderDefines("SIGNED_DISTANCE_FIELD TEXT_EFFECT_SHADOW");
+                        break;
+
+                    case TE_STROKE:
+                        pass->SetPixelShaderDefines("SIGNED_DISTANCE_FIELD TEXT_EFFECT_STROKE");
+                        break;
+                    }
+                }
+            }
+
+            switch (GetTextEffect())
+            {
+            case TE_SHADOW:
+                if (texture)
+                {
+                    Vector2 shadowOffset(0.5f / texture->GetWidth(), 0.5f / texture->GetHeight());
+                    material->SetShaderParameter("ShadowOffset", shadowOffset);
+                }
+                material->SetShaderParameter("ShadowColor", GetEffectColor());
+                break;
+
+            case TE_STROKE:
+                material->SetShaderParameter("StrokeColor", GetEffectColor());
+                break;
+
+            default:
+                break;
+            }
+        }
+        else
+        {
+            // If not SDF, set shader defines based on whether font texture is full RGB or just alpha
+            if (!material_)
+            {
+                Technique* tech = material->GetTechnique(0);
+                Pass* pass = tech ? tech->GetPass("alpha") : (Pass*)0;
+                if (pass)
+                {
+                    if (texture && texture->GetFormat() == Graphics::GetAlphaFormat())
+                        pass->SetPixelShaderDefines("ALPHAMAP");
+                    else
+                        pass->SetPixelShaderDefines("");
+                }
+            }
+        }
+    }
+}
+
+void Text3D::CalculateFixedScreenSize(const FrameInfo& frame)
+{
+    Vector3 worldPosition = node_->GetWorldPosition();
+    Vector3 worldScale = node_->GetWorldScale();
+
+    if (fixedScreenSize_)
+    {
+        float textScaling = 2.0f / TEXT_SCALING / frame.viewSize_.y_;
+        float halfViewWorldSize = frame.camera_->GetHalfViewSize();
+
+        if (!frame.camera_->IsOrthographic())
+        {
+            Matrix4 viewProj(frame.camera_->GetProjection() * frame.camera_->GetView());
+            Vector4 projPos(viewProj * Vector4(worldPosition, 1.0f));
+            worldScale *= textScaling * halfViewWorldSize * projPos.w_;
+        }
+        else
+            worldScale *= textScaling * halfViewWorldSize;
+    }
+
+    customWorldTransform_ = Matrix3x4(worldPosition, frame.camera_->GetFaceCameraRotation(
+        worldPosition, node_->GetWorldRotation(), faceCameraMode_, minAngle_), worldScale);
+    worldBoundingBoxDirty_ = true;
+}
+
+
+Text3DHorizontalAlignment Text3D::GetHorizontalAlignment() const
+{
+    return horizontalAlignment_;
+}
+
+Text3DVerticalAlignment Text3D::GetVerticalAlignment() const
+{
+    return verticalAlignment_;
+}
+
+void Text3D::SetAlignment(Text3DHorizontalAlignment hAlign, Text3DVerticalAlignment vAlign)
+{
+    horizontalAlignment_ = hAlign;
+    verticalAlignment_ = vAlign;
+
+    MarkTextDirty();
+}
+
+void Text3D::SetHorizontalAlignment(Text3DHorizontalAlignment align)
+{
+    horizontalAlignment_ = align;
+    MarkTextDirty();
+}
+
+void Text3D::SetVerticalAlignment(Text3DVerticalAlignment align)
+{
+    verticalAlignment_ = align;
+    MarkTextDirty();
+}
+
+}

+ 225 - 0
Source/Atomic/Graphics/Text3D/Text3D.h

@@ -0,0 +1,225 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../../Graphics/Drawable.h"
+#include "../../Graphics/VertexBuffer.h"
+#include "../../Math/Matrix3x4.h"
+#include "Text3DText.h"
+
+namespace Atomic
+{
+
+class Text3DText;
+
+/// 3D text component.
+class ATOMIC_API Text3D : public Drawable
+{
+    ATOMIC_OBJECT(Text3D, Drawable)
+
+public:
+    /// Construct.
+    Text3D(Context* context);
+    /// Destruct.
+    ~Text3D();
+    /// Register object factory. Drawable must be registered first.
+    static void RegisterObject(Context* context);
+
+    /// Apply attribute changes that can not be applied immediately.
+    virtual void ApplyAttributes();
+    /// Calculate distance and prepare batches for rendering. May be called from worker thread(s), possibly re-entrantly.
+    virtual void UpdateBatches(const FrameInfo& frame);
+    /// Prepare geometry for rendering. Called from a worker thread if possible (no GPU update.)
+    virtual void UpdateGeometry(const FrameInfo& frame);
+    /// Return whether a geometry update is necessary, and if it can happen in a worker thread.
+    virtual UpdateGeometryType GetUpdateGeometryType();
+
+    /// Set font by looking from resource cache by name and font size. Return true if successful.
+    bool SetFont(const String& fontName, int size = TEXT3D_DEFAULT_FONT_SIZE);
+    /// Set font and font size. Return true if successful.
+    bool SetFont(Text3DFont* font, int size = TEXT3D_DEFAULT_FONT_SIZE);
+    /// Set font size only while retaining the existing font. Return true if successful.
+    bool SetFontSize(int size);
+    /// Set material.
+    void SetMaterial(Material* material);
+    /// Set text. Text is assumed to be either ASCII or UTF8-encoded.
+    void SetText(const String& text);
+    /// Set horizontal and vertical alignment.
+    void SetAlignment(Text3DHorizontalAlignment hAlign, Text3DVerticalAlignment vAlign);
+    /// Set horizontal alignment.
+    void SetHorizontalAlignment(Text3DHorizontalAlignment align);
+    /// Set vertical alignment.
+    void SetVerticalAlignment(Text3DVerticalAlignment align);
+    /// Set row alignment.
+    void SetTextAlignment(Text3DHorizontalAlignment align);
+    /// Set row spacing, 1.0 for original font spacing.
+    void SetRowSpacing(float spacing);
+    /// Set wordwrap. In wordwrap mode the text element will respect its current width. Otherwise it resizes itself freely.
+    void SetWordwrap(bool enable);
+    /// Set text effect.
+    void SetTextEffect(Text3DTextEffect textEffect);
+    /// Set shadow offset.
+    void SetEffectShadowOffset(const IntVector2& offset);
+    /// Set stroke thickness.
+    void SetEffectStrokeThickness(int thickness);
+    /// Set stroke rounding. Corners of the font will be rounded off in the stroke so the stroke won't have corners.
+    void SetEffectRoundStroke(bool roundStroke);
+    /// Set effect color.
+    void SetEffectColor(const Color& effectColor);
+    /// Set effect Z bias.
+    void SetEffectDepthBias(float bias);
+    /// Set text width. Only has effect in word wrap mode.
+    void SetWidth(int width);
+    /// Set color on all corners.
+    void SetColor(const Color& color);
+    /// Set color on one corner.
+    void SetColor(Text3DCorner corner, const Color& color);
+    /// Set opacity.
+    void SetOpacity(float opacity);
+    /// Set whether text has fixed size on screen (pixel-perfect) regardless of distance to camera. Works best when combined with face camera rotation. Default false.
+    void SetFixedScreenSize(bool enable);
+    /// Set how the text should rotate in relation to the camera. Default is to not rotate (FC_NONE.)
+    void SetFaceCameraMode(FaceCameraMode mode);
+
+    /// Return font.
+    Text3DFont* GetFont() const;
+    /// Return font size.
+    int GetFontSize() const;
+    /// Return material.
+    Material* GetMaterial() const;
+    /// Return text.
+    const String& GetText() const;
+    /// Return row alignment.
+    Text3DHorizontalAlignment GetTextAlignment() const;
+    /// Return horizontal alignment.
+    Text3DHorizontalAlignment GetHorizontalAlignment() const;
+    /// Return vertical alignment.
+    Text3DVerticalAlignment GetVerticalAlignment() const;
+    /// Return row spacing.
+    float GetRowSpacing() const;
+    /// Return wordwrap mode.
+    bool GetWordwrap() const;
+    /// Return text effect.
+    Text3DTextEffect GetTextEffect() const;
+    /// Return effect shadow offset.
+    const IntVector2& GetEffectShadowOffset() const;
+    /// Return effect stroke thickness.
+    int GetEffectStrokeThickness() const;
+    /// Return effect round stroke.
+    bool GetEffectRoundStroke() const;
+    /// Return effect color.
+    const Color& GetEffectColor() const;
+    /// Return effect depth bias.
+    float GetEffectDepthBias() const;
+    /// Return text width.
+    int GetWidth() const;
+    /// Return text height.
+    int GetHeight() const;
+    /// Return row height.
+    int GetRowHeight() const;
+    /// Return number of rows.
+    unsigned GetNumRows() const;
+    /// Return number of characters.
+    unsigned GetNumChars() const;
+    /// Return width of row by index.
+    int GetRowWidth(unsigned index) const;
+    /// Return position of character by index relative to the text element origin.
+    IntVector2 GetCharPosition(unsigned index);
+    /// Return size of character by index.
+    IntVector2 GetCharSize(unsigned index);
+    /// Return corner color.
+    const Color& GetColor(Text3DCorner corner) const;
+    /// Return opacity.
+    float GetOpacity() const;
+    /// Return whether text has fixed screen size.
+    bool IsFixedScreenSize() const { return fixedScreenSize_; }
+    /// Return how the text rotates in relation to the camera.
+    FaceCameraMode GetFaceCameraMode() const { return faceCameraMode_; }
+
+    /// Set font attribute.
+    void SetFontAttr(const ResourceRef& value);
+    /// Return font attribute.
+    ResourceRef GetFontAttr() const;
+    /// Set material attribute.
+    void SetMaterialAttr(const ResourceRef& value);
+    /// Return material attribute.
+    ResourceRef GetMaterialAttr() const;
+    /// Set text attribute.
+    void SetTextAttr(const String& value);
+    /// Return text attribute.
+    String GetTextAttr() const;
+
+    /// Get color attribute. Uses just the top-left color.
+    const Color& GetColorAttr() const { return text_.color_[0]; }
+
+protected:
+    /// Handle node being assigned.
+    virtual void OnNodeSet(Node* node);
+    /// Recalculate the world-space bounding box.
+    virtual void OnWorldBoundingBoxUpdate();
+    /// Mark text & geometry dirty.
+    void MarkTextDirty();
+    /// Update text %UI batches.
+    void UpdateTextBatches();
+    /// Create materials for text rendering. May only be called from the main thread. Text %UI batches must be up-to-date.
+    void UpdateTextMaterials(bool forceUpdate = false);
+    /// Recalculate camera facing and fixed screen size.
+    void CalculateFixedScreenSize(const FrameInfo& frame);
+    
+    /// Internally used text element.
+    Text3DText text_;
+    /// Geometries.
+    Vector<SharedPtr<Geometry> > geometries_;
+    /// Vertex buffer.
+    SharedPtr<VertexBuffer> vertexBuffer_;
+    /// Material to use as a base for the text material(s).
+    SharedPtr<Material> material_;
+    /// Text UI batches.
+    PODVector<Text3DBatch> uiBatches_;
+    /// Text vertex data.
+    PODVector<float> uiVertexData_;
+    /// Custom world transform for facing the camera automatically.
+    Matrix3x4 customWorldTransform_;
+    /// Text rotation mode in relation to the camera.
+    FaceCameraMode faceCameraMode_;
+    /// Minimal angle between text normal and look-at direction.
+    float minAngle_;
+    /// Fixed screen size flag.
+    bool fixedScreenSize_;
+    /// Text needs update flag.
+    bool textDirty_;
+    /// Geometry dirty flag.
+    bool geometryDirty_;
+    /// Flag for whether currently using SDF shader defines in the generated material.
+    bool usingSDFShader_;
+    /// Font texture data lost flag.
+    bool fontDataLost_;
+
+    Text3DHorizontalAlignment horizontalAlignment_;
+    Text3DVerticalAlignment verticalAlignment_;
+
+
+};
+
+}
+

+ 458 - 0
Source/Atomic/Graphics/Text3D/Text3DBatch.cpp

@@ -0,0 +1,458 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../Graphics.h"
+#include "../Texture.h"
+#include "Text3DText.h"
+#include "Text3DBatch.h"
+
+#include "../../DebugNew.h"
+
+namespace Atomic
+{
+
+Vector3 Text3DBatch::posAdjust(0.0f, 0.0f, 0.0f);
+
+Text3DBatch::Text3DBatch() :
+    element_(0),
+    blendMode_(BLEND_REPLACE),
+    texture_(0),
+    invTextureSize_(Vector2::ONE),
+    vertexData_(0),
+    vertexStart_(0),
+    vertexEnd_(0),
+    useGradient_(false)
+{
+    SetDefaultColor();
+}
+
+Text3DBatch::Text3DBatch(Text3DText* element, BlendMode blendMode, const IntRect& scissor, Texture* texture, PODVector<float>* vertexData) :
+    element_(element),
+    blendMode_(blendMode),
+    scissor_(scissor),
+    texture_(texture),
+    invTextureSize_(texture ? Vector2(1.0f / (float)texture->GetWidth(), 1.0f / (float)texture->GetHeight()) : Vector2::ONE),
+    vertexData_(vertexData),
+    vertexStart_(vertexData->Size()),
+    vertexEnd_(vertexData->Size())
+{
+    SetDefaultColor();
+}
+
+void Text3DBatch::SetColor(const Color& color)
+{
+    color_ = color.ToUInt();
+}
+
+void Text3DBatch::SetDefaultColor()
+{
+    if (element_)
+    {
+        color_ = element_->GetColor().ToUInt();
+        useGradient_ = element_->HasColorGradient();
+    }
+    else
+    {
+        color_ = 0xffffffff;
+        useGradient_ = false;
+    }
+}
+
+
+unsigned Text3DBatch::GetInterpolatedColor(int x, int y)
+{
+    const IntVector2& size = element_->GetSize();
+
+    if (size.x_ && size.y_)
+    {
+        float cLerpX = Clamp((float)x / (float)size.x_, 0.0f, 1.0f);
+        float cLerpY = Clamp((float)y / (float)size.y_, 0.0f, 1.0f);
+
+        Color topColor = element_->GetColor(C_TOPLEFT).Lerp(element_->GetColor(C_TOPRIGHT), cLerpX);
+        Color bottomColor = element_->GetColor(C_BOTTOMLEFT).Lerp(element_->GetColor(C_BOTTOMRIGHT), cLerpX);
+        Color color = topColor.Lerp(bottomColor, cLerpY);
+        color.a_ *= element_->GetOpacity();
+        return color.ToUInt();
+    }
+    else
+    {
+        Color color = element_->GetColor(C_TOPLEFT);
+        color.a_ *= element_->GetOpacity();
+        return color.ToUInt();
+    }
+}
+
+
+void Text3DBatch::AddQuad(int x, int y, int width, int height, int texOffsetX, int texOffsetY, int texWidth, int texHeight)
+{
+    unsigned topLeftColor, topRightColor, bottomLeftColor, bottomRightColor;
+
+    if (!useGradient_)
+    {
+        // If alpha is 0, nothing will be rendered, so do not add the quad
+        if (!(color_ & 0xff000000))
+            return;
+
+        topLeftColor = color_;
+        topRightColor = color_;
+        bottomLeftColor = color_;
+        bottomRightColor = color_;
+    }
+    else
+    {
+        topLeftColor = GetInterpolatedColor(x, y);
+        topRightColor = GetInterpolatedColor(x + width, y);
+        bottomLeftColor = GetInterpolatedColor(x, y + height);
+        bottomRightColor = GetInterpolatedColor(x + width, y + height);
+    }
+
+    const IntVector2& screenPos = IntVector2::ZERO; //element_->GetScreenPosition();
+
+    float left = (float)(x + screenPos.x_) - posAdjust.x_;
+    float right = left + (float)width;
+    float top = (float)(y + screenPos.y_) - posAdjust.x_;
+    float bottom = top + (float)height;
+
+    float leftUV = texOffsetX * invTextureSize_.x_;
+    float topUV = texOffsetY * invTextureSize_.y_;
+    float rightUV = (texOffsetX + (texWidth ? texWidth : width)) * invTextureSize_.x_;
+    float bottomUV = (texOffsetY + (texHeight ? texHeight : height)) * invTextureSize_.y_;
+
+    unsigned begin = vertexData_->Size();
+    vertexData_->Resize(begin + 6 * TEXT3D_VERTEX_SIZE);
+    float* dest = &(vertexData_->At(begin));
+    vertexEnd_ = vertexData_->Size();
+
+    dest[0] = left;
+    dest[1] = top;
+    dest[2] = 0.0f;
+    ((unsigned&)dest[3]) = topLeftColor;
+    dest[4] = leftUV;
+    dest[5] = topUV;
+
+    dest[6] = right;
+    dest[7] = top;
+    dest[8] = 0.0f;
+    ((unsigned&)dest[9]) = topRightColor;
+    dest[10] = rightUV;
+    dest[11] = topUV;
+
+    dest[12] = left;
+    dest[13] = bottom;
+    dest[14] = 0.0f;
+    ((unsigned&)dest[15]) = bottomLeftColor;
+    dest[16] = leftUV;
+    dest[17] = bottomUV;
+
+    dest[18] = right;
+    dest[19] = top;
+    dest[20] = 0.0f;
+    ((unsigned&)dest[21]) = topRightColor;
+    dest[22] = rightUV;
+    dest[23] = topUV;
+
+    dest[24] = right;
+    dest[25] = bottom;
+    dest[26] = 0.0f;
+    ((unsigned&)dest[27]) = bottomRightColor;
+    dest[28] = rightUV;
+    dest[29] = bottomUV;
+
+    dest[30] = left;
+    dest[31] = bottom;
+    dest[32] = 0.0f;
+    ((unsigned&)dest[33]) = bottomLeftColor;
+    dest[34] = leftUV;
+    dest[35] = bottomUV;
+}
+
+void Text3DBatch::AddQuad(const Matrix3x4& transform, int x, int y, int width, int height, int texOffsetX, int texOffsetY,
+    int texWidth, int texHeight)
+{
+    unsigned topLeftColor, topRightColor, bottomLeftColor, bottomRightColor;
+
+    if (!useGradient_)
+    {
+        // If alpha is 0, nothing will be rendered, so do not add the quad
+        if (!(color_ & 0xff000000))
+            return;
+
+        topLeftColor = color_;
+        topRightColor = color_;
+        bottomLeftColor = color_;
+        bottomRightColor = color_;
+    }
+    else
+    {
+        topLeftColor = GetInterpolatedColor(x, y);
+        topRightColor = GetInterpolatedColor(x + width, y);
+        bottomLeftColor = GetInterpolatedColor(x, y + height);
+        bottomRightColor = GetInterpolatedColor(x + width, y + height);
+    }
+
+    Vector3 v1 = (transform * Vector3((float)x, (float)y, 0.0f)) - posAdjust;
+    Vector3 v2 = (transform * Vector3((float)x + (float)width, (float)y, 0.0f)) - posAdjust;
+    Vector3 v3 = (transform * Vector3((float)x, (float)y + (float)height, 0.0f)) - posAdjust;
+    Vector3 v4 = (transform * Vector3((float)x + (float)width, (float)y + (float)height, 0.0f)) - posAdjust;
+
+    float leftUV = ((float)texOffsetX) * invTextureSize_.x_;
+    float topUV = ((float)texOffsetY) * invTextureSize_.y_;
+    float rightUV = ((float)(texOffsetX + (texWidth ? texWidth : width))) * invTextureSize_.x_;
+    float bottomUV = ((float)(texOffsetY + (texHeight ? texHeight : height))) * invTextureSize_.y_;
+
+    unsigned begin = vertexData_->Size();
+    vertexData_->Resize(begin + 6 * TEXT3D_VERTEX_SIZE);
+    float* dest = &(vertexData_->At(begin));
+    vertexEnd_ = vertexData_->Size();
+
+    dest[0] = v1.x_;
+    dest[1] = v1.y_;
+    dest[2] = 0.0f;
+    ((unsigned&)dest[3]) = topLeftColor;
+    dest[4] = leftUV;
+    dest[5] = topUV;
+
+    dest[6] = v2.x_;
+    dest[7] = v2.y_;
+    dest[8] = 0.0f;
+    ((unsigned&)dest[9]) = topRightColor;
+    dest[10] = rightUV;
+    dest[11] = topUV;
+
+    dest[12] = v3.x_;
+    dest[13] = v3.y_;
+    dest[14] = 0.0f;
+    ((unsigned&)dest[15]) = bottomLeftColor;
+    dest[16] = leftUV;
+    dest[17] = bottomUV;
+
+    dest[18] = v2.x_;
+    dest[19] = v2.y_;
+    dest[20] = 0.0f;
+    ((unsigned&)dest[21]) = topRightColor;
+    dest[22] = rightUV;
+    dest[23] = topUV;
+
+    dest[24] = v4.x_;
+    dest[25] = v4.y_;
+    dest[26] = 0.0f;
+    ((unsigned&)dest[27]) = bottomRightColor;
+    dest[28] = rightUV;
+    dest[29] = bottomUV;
+
+    dest[30] = v3.x_;
+    dest[31] = v3.y_;
+    dest[32] = 0.0f;
+    ((unsigned&)dest[33]) = bottomLeftColor;
+    dest[34] = leftUV;
+    dest[35] = bottomUV;
+}
+
+void Text3DBatch::AddQuad(int x, int y, int width, int height, int texOffsetX, int texOffsetY, int texWidth, int texHeight, bool tiled)
+{
+    if (!(element_->HasColorGradient() || element_->GetColor().ToUInt() & 0xff000000))
+        return; // No gradient and alpha is 0, so do not add the quad
+
+    if (!tiled)
+    {
+        AddQuad(x, y, width, height, texOffsetX, texOffsetY, texWidth, texHeight);
+        return;
+    }
+
+    int tileX = 0;
+    int tileY = 0;
+    int tileW = 0;
+    int tileH = 0;
+
+    while (tileY < height)
+    {
+        tileX = 0;
+        tileH = Min(height - tileY, texHeight);
+
+        while (tileX < width)
+        {
+            tileW = Min(width - tileX, texWidth);
+
+            AddQuad(x + tileX, y + tileY, tileW, tileH, texOffsetX, texOffsetY, tileW, tileH);
+
+            tileX += tileW;
+        }
+
+        tileY += tileH;
+    }
+}
+
+void Text3DBatch::AddQuad(const Matrix3x4& transform, const IntVector2& a, const IntVector2& b, const IntVector2& c, const IntVector2& d,
+    const IntVector2& texA, const IntVector2& texB, const IntVector2& texC, const IntVector2& texD)
+{
+    Vector3 v1 = (transform * Vector3((float)a.x_, (float)a.y_, 0.0f)) - posAdjust;
+    Vector3 v2 = (transform * Vector3((float)b.x_, (float)b.y_, 0.0f)) - posAdjust;
+    Vector3 v3 = (transform * Vector3((float)c.x_, (float)c.y_, 0.0f)) - posAdjust;
+    Vector3 v4 = (transform * Vector3((float)d.x_, (float)d.y_, 0.0f)) - posAdjust;
+
+    Vector2 uv1((float)texA.x_ * invTextureSize_.x_, (float)texA.y_ * invTextureSize_.y_);
+    Vector2 uv2((float)texB.x_ * invTextureSize_.x_, (float)texB.y_ * invTextureSize_.y_);
+    Vector2 uv3((float)texC.x_ * invTextureSize_.x_, (float)texC.y_ * invTextureSize_.y_);
+    Vector2 uv4((float)texD.x_ * invTextureSize_.x_, (float)texD.y_ * invTextureSize_.y_);
+
+    unsigned begin = vertexData_->Size();
+    vertexData_->Resize(begin + 6 * TEXT3D_VERTEX_SIZE);
+    float* dest = &(vertexData_->At(begin));
+    vertexEnd_ = vertexData_->Size();
+
+    dest[0] = v1.x_;
+    dest[1] = v1.y_;
+    dest[2] = 0.0f;
+    ((unsigned&)dest[3]) = color_;
+    dest[4] = uv1.x_;
+    dest[5] = uv1.y_;
+
+    dest[6] = v2.x_;
+    dest[7] = v2.y_;
+    dest[8] = 0.0f;
+    ((unsigned&)dest[9]) = color_;
+    dest[10] = uv2.x_;
+    dest[11] = uv2.y_;
+    
+    dest[12] = v3.x_;
+    dest[13] = v3.y_;
+    dest[14] = 0.0f;
+    ((unsigned&)dest[15]) = color_;
+    dest[16] = uv3.x_;
+    dest[17] = uv3.y_;
+
+    dest[18] = v1.x_;
+    dest[19] = v1.y_;
+    dest[20] = 0.0f;
+    ((unsigned&)dest[21]) = color_;
+    dest[22] = uv1.x_;
+    dest[23] = uv1.y_;
+
+    dest[24] = v3.x_;
+    dest[25] = v3.y_;
+    dest[26] = 0.0f;
+    ((unsigned&)dest[27]) = color_;
+    dest[28] = uv3.x_;
+    dest[29] = uv3.y_;
+
+    dest[30] = v4.x_;
+    dest[31] = v4.y_;
+    dest[32] = 0.0f;
+    ((unsigned&)dest[33]) = color_;
+    dest[34] = uv4.x_;
+    dest[35] = uv4.y_;
+}
+
+void Text3DBatch::AddQuad(const Matrix3x4& transform, const IntVector2& a, const IntVector2& b, const IntVector2& c, const IntVector2& d,
+    const IntVector2& texA, const IntVector2& texB, const IntVector2& texC, const IntVector2& texD, const Color& colA,
+    const Color& colB, const Color& colC, const Color& colD)
+{
+    Vector3 v1 = (transform * Vector3((float)a.x_, (float)a.y_, 0.0f)) - posAdjust;
+    Vector3 v2 = (transform * Vector3((float)b.x_, (float)b.y_, 0.0f)) - posAdjust;
+    Vector3 v3 = (transform * Vector3((float)c.x_, (float)c.y_, 0.0f)) - posAdjust;
+    Vector3 v4 = (transform * Vector3((float)d.x_, (float)d.y_, 0.0f)) - posAdjust;
+
+    Vector2 uv1((float)texA.x_ * invTextureSize_.x_, (float)texA.y_ * invTextureSize_.y_);
+    Vector2 uv2((float)texB.x_ * invTextureSize_.x_, (float)texB.y_ * invTextureSize_.y_);
+    Vector2 uv3((float)texC.x_ * invTextureSize_.x_, (float)texC.y_ * invTextureSize_.y_);
+    Vector2 uv4((float)texD.x_ * invTextureSize_.x_, (float)texD.y_ * invTextureSize_.y_);
+
+    unsigned c1 = colA.ToUInt();
+    unsigned c2 = colB.ToUInt();
+    unsigned c3 = colC.ToUInt();
+    unsigned c4 = colD.ToUInt();
+
+    unsigned begin = vertexData_->Size();
+    vertexData_->Resize(begin + 6 * TEXT3D_VERTEX_SIZE);
+    float* dest = &(vertexData_->At(begin));
+    vertexEnd_ = vertexData_->Size();
+
+    dest[0] = v1.x_;
+    dest[1] = v1.y_;
+    dest[2] = 0.0f;
+    ((unsigned&)dest[3]) = c1;
+    dest[4] = uv1.x_;
+    dest[5] = uv1.y_;
+
+    dest[6] = v2.x_;
+    dest[7] = v2.y_;
+    dest[8] = 0.0f;
+    ((unsigned&)dest[9]) = c2;
+    dest[10] = uv2.x_;
+    dest[11] = uv2.y_;
+    
+    dest[12] = v3.x_;
+    dest[13] = v3.y_;
+    dest[14] = 0.0f;
+    ((unsigned&)dest[15]) = c3;
+    dest[16] = uv3.x_;
+    dest[17] = uv3.y_;
+
+    dest[18] = v1.x_;
+    dest[19] = v1.y_;
+    dest[20] = 0.0f;
+    ((unsigned&)dest[21]) = c1;
+    dest[22] = uv1.x_;
+    dest[23] = uv1.y_;
+
+    dest[24] = v3.x_;
+    dest[25] = v3.y_;
+    dest[26] = 0.0f;
+    ((unsigned&)dest[27]) = c3;
+    dest[28] = uv3.x_;
+    dest[29] = uv3.y_;
+
+    dest[30] = v4.x_;
+    dest[31] = v4.y_;
+    dest[32] = 0.0f;
+    ((unsigned&)dest[33]) = c4;
+    dest[34] = uv4.x_;
+    dest[35] = uv4.y_;
+}
+
+bool Text3DBatch::Merge(const Text3DBatch& batch)
+{
+    if (batch.blendMode_ != blendMode_ ||
+        batch.scissor_ != scissor_ ||
+        batch.texture_ != texture_ ||
+        batch.vertexData_ != vertexData_ ||
+        batch.vertexStart_ != vertexEnd_)
+        return false;
+
+    vertexEnd_ = batch.vertexEnd_;
+    return true;
+}
+
+void Text3DBatch::AddOrMerge(const Text3DBatch& batch, PODVector<Text3DBatch>& batches)
+{
+    if (batch.vertexEnd_ == batch.vertexStart_)
+        return;
+
+    if (!batches.Empty() && batches.Back().Merge(batch))
+        return;
+
+    batches.Push(batch);
+}
+
+}

+ 103 - 0
Source/Atomic/Graphics/Text3D/Text3DBatch.h

@@ -0,0 +1,103 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../../Math/Color.h"
+#include "../../Math/Rect.h"
+#include "../GraphicsDefs.h"
+
+namespace Atomic
+{
+
+class PixelShader;
+class Graphics;
+class Matrix3x4;
+class Texture;
+class Text3DText;
+
+static const unsigned TEXT3D_VERTEX_SIZE = 6;
+
+/// Text rendering draw call.
+class ATOMIC_API Text3DBatch
+{
+public:
+    /// Construct with defaults.
+    Text3DBatch();
+    /// Construct.
+    Text3DBatch(Text3DText* element, BlendMode blendMode, const IntRect& scissor, Texture* texture, PODVector<float>* vertexData);
+
+    /// Set new color for the batch. Overrides gradient.
+    void SetColor(const Color& color);
+    /// Restore UI element's default color.
+    void SetDefaultColor();
+    /// Add a quad.
+    void AddQuad(int x, int y, int width, int height, int texOffsetX, int texOffsetY, int texWidth = 0, int texHeight = 0);
+    /// Add a quad using a transform matrix.
+    void AddQuad(const Matrix3x4& transform, int x, int y, int width, int height, int texOffsetX, int texOffsetY, int texWidth = 0,
+        int texHeight = 0);
+    /// Add a quad with tiled texture.
+    void AddQuad(int x, int y, int width, int height, int texOffsetX, int texOffsetY, int texWidth, int texHeight, bool tiled);
+    /// Add a quad with freeform points and UVs. Uses the current color, not gradient. Points should be specified in clockwise order.
+    void AddQuad(const Matrix3x4& transform, const IntVector2& a, const IntVector2& b, const IntVector2& c, const IntVector2& d,
+        const IntVector2& texA, const IntVector2& texB, const IntVector2& texC, const IntVector2& texD);
+    /// Add a quad with freeform points, UVs and colors. Points should be specified in clockwise order.
+    void AddQuad(const Matrix3x4& transform, const IntVector2& a, const IntVector2& b, const IntVector2& c, const IntVector2& d,
+        const IntVector2& texA, const IntVector2& texB, const IntVector2& texC, const IntVector2& texD, const Color& colA,
+        const Color& colB, const Color& colC, const Color& colD);
+    /// Merge with another batch.
+    bool Merge(const Text3DBatch& batch);
+    /// Return an interpolated color for the element.
+    unsigned GetInterpolatedColor(int x, int y);
+
+
+    /// Add or merge a batch.
+    static void AddOrMerge(const Text3DBatch& batch, PODVector<Text3DBatch>& batches);
+
+    /// Element this batch represents.
+    Text3DText* element_;
+    /// Blending mode.
+    BlendMode blendMode_;
+    /// Scissor rectangle.
+    IntRect scissor_;
+    /// Texture.
+    Texture* texture_;
+    /// Inverse texture size.
+    Vector2 invTextureSize_;
+    /// Current color. By default calculated from the element.
+    unsigned color_;
+    /// Vertex data.
+    PODVector<float>* vertexData_;
+    /// Vertex data start index.
+    unsigned vertexStart_;
+    /// Vertex data end index.
+    unsigned vertexEnd_;
+
+    /// Position adjustment vector for pixel-perfect rendering. Initialized by UI.
+    static Vector3 posAdjust;
+
+    /// Gradient flag.
+    bool useGradient_;
+
+};
+
+}

+ 388 - 0
Source/Atomic/Graphics/Text3D/Text3DBitmap.cpp

@@ -0,0 +1,388 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../../Core/Context.h"
+#include "../../Graphics/Graphics.h"
+#include "../../Graphics/Texture2D.h"
+#include "../../IO/File.h"
+#include "../../IO/FileSystem.h"
+#include "../../IO/Log.h"
+#include "../../IO/MemoryBuffer.h"
+#include "../../Resource/ResourceCache.h"
+#include "../../Resource/XMLFile.h"
+#include "Text3DFont.h"
+#include "Text3DBitmap.h"
+
+#include "../../DebugNew.h"
+
+namespace Atomic
+{
+
+Text3DBitmap::Text3DBitmap(Text3DFont* font) :
+    Text3DFontFace(font)
+{
+}
+
+Text3DBitmap::~Text3DBitmap()
+{
+}
+
+bool Text3DBitmap::Load(const unsigned char* fontData, unsigned fontDataSize, int pointSize)
+{
+    Context* context = font_->GetContext();
+
+    SharedPtr<XMLFile> xmlReader(new XMLFile(context));
+    MemoryBuffer memoryBuffer(fontData, fontDataSize);
+    if (!xmlReader->Load(memoryBuffer))
+    {
+        ATOMIC_LOGERROR("Could not load XML file");
+        return false;
+    }
+
+    XMLElement root = xmlReader->GetRoot("font");
+    if (root.IsNull())
+    {
+        ATOMIC_LOGERROR("Could not find Font element");
+        return false;
+    }
+
+    XMLElement pagesElem = root.GetChild("pages");
+    if (pagesElem.IsNull())
+    {
+        ATOMIC_LOGERROR("Could not find Pages element");
+        return false;
+    }
+
+    XMLElement infoElem = root.GetChild("info");
+    if (!infoElem.IsNull())
+        pointSize_ = infoElem.GetInt("size");
+
+    XMLElement commonElem = root.GetChild("common");
+    rowHeight_ = commonElem.GetInt("lineHeight");
+    unsigned pages = commonElem.GetUInt("pages");
+    textures_.Reserve(pages);
+
+    ResourceCache* resourceCache = font_->GetSubsystem<ResourceCache>();
+    String fontPath = GetPath(font_->GetName());
+    unsigned totalTextureSize = 0;
+
+    XMLElement pageElem = pagesElem.GetChild("page");
+    for (unsigned i = 0; i < pages; ++i)
+    {
+        if (pageElem.IsNull())
+        {
+            ATOMIC_LOGERROR("Could not find Page element for page: " + String(i));
+            return 0;
+        }
+
+        // Assume the font image is in the same directory as the font description file
+        String textureFile = fontPath + pageElem.GetAttribute("file");
+
+        // Load texture manually to allow controlling the alpha channel mode
+        SharedPtr<File> fontFile = resourceCache->GetFile(textureFile);
+        SharedPtr<Image> fontImage(new Image(context));
+        if (!fontFile || !fontImage->Load(*fontFile))
+        {
+            ATOMIC_LOGERROR("Failed to load font image file");
+            return 0;
+        }
+        SharedPtr<Texture2D> texture = LoadFaceTexture(fontImage);
+        if (!texture)
+            return 0;
+
+        textures_.Push(texture);
+
+        // Add texture to resource cache
+        texture->SetName(fontFile->GetName());
+        resourceCache->AddManualResource(texture);
+
+        totalTextureSize += fontImage->GetWidth() * fontImage->GetHeight() * fontImage->GetComponents();
+
+        pageElem = pageElem.GetNext("page");
+    }
+
+    XMLElement charsElem = root.GetChild("chars");
+    int count = charsElem.GetInt("count");
+
+    XMLElement charElem = charsElem.GetChild("char");
+    while (!charElem.IsNull())
+    {
+        int id = charElem.GetInt("id");
+
+        Text3DFontGlyph glyph;
+        glyph.x_ = (short)charElem.GetInt("x");
+        glyph.y_ = (short)charElem.GetInt("y");
+        glyph.width_ = (short)charElem.GetInt("width");
+        glyph.height_ = (short)charElem.GetInt("height");
+        glyph.offsetX_ = (short)charElem.GetInt("xoffset");
+        glyph.offsetY_ = (short)charElem.GetInt("yoffset");
+        glyph.advanceX_ = (short)charElem.GetInt("xadvance");
+        glyph.page_ = charElem.GetUInt("page");
+
+        glyphMapping_[id] = glyph;
+
+        charElem = charElem.GetNext("char");
+    }
+
+    XMLElement kerningsElem = root.GetChild("kernings");
+    if (kerningsElem.NotNull())
+    {
+        XMLElement kerningElem = kerningsElem.GetChild("kerning");
+        while (!kerningElem.IsNull())
+        {
+            int first = kerningElem.GetInt("first");
+            int second = kerningElem.GetInt("second");
+            unsigned value = (unsigned)((first << 16) + second);
+            kerningMapping_[value] = (short)kerningElem.GetInt("amount");
+
+            kerningElem = kerningElem.GetNext("kerning");
+        }
+    }
+
+    ATOMIC_LOGDEBUGF("Bitmap font face %s has %d glyphs", GetFileName(font_->GetName()).CString(), count);
+
+    font_->SetMemoryUse(font_->GetMemoryUse() + totalTextureSize);
+    return true;
+}
+
+bool Text3DBitmap::Load(Text3DFontFace* fontFace, bool usedGlyphs)
+{
+    if (this == fontFace)
+        return true;
+
+    if (!usedGlyphs)
+    {
+        glyphMapping_ = fontFace->glyphMapping_;
+        kerningMapping_ = fontFace->kerningMapping_;
+        textures_ = fontFace->textures_;
+        pointSize_ = fontFace->pointSize_;
+        rowHeight_ = fontFace->rowHeight_;
+
+        return true;
+    }
+
+    pointSize_ = fontFace->pointSize_;
+    rowHeight_ = fontFace->rowHeight_;
+
+    unsigned numPages = 1;
+
+    int maxTextureSize = TEXT3D_DEFAULT_FONT_TEXTURE_MAX_SIZE;// font_->GetSubsystem<UI>()->GetMaxFontTextureSize();
+
+    AreaAllocator allocator(TEXT3D_FONT_TEXTURE_MIN_SIZE, TEXT3D_FONT_TEXTURE_MIN_SIZE, maxTextureSize, maxTextureSize);
+
+    for (HashMap<unsigned, Text3DFontGlyph>::ConstIterator i = fontFace->glyphMapping_.Begin(); i != fontFace->glyphMapping_.End(); ++i)
+    {
+        Text3DFontGlyph fontGlyph = i->second_;
+        if (!fontGlyph.used_)
+            continue;
+
+        int x, y;
+        if (!allocator.Allocate(fontGlyph.width_ + 1, fontGlyph.height_ + 1, x, y))
+        {
+            ++numPages;
+
+            allocator = AreaAllocator(TEXT3D_FONT_TEXTURE_MIN_SIZE, TEXT3D_FONT_TEXTURE_MIN_SIZE, maxTextureSize, maxTextureSize);
+            if (!allocator.Allocate(fontGlyph.width_ + 1, fontGlyph.height_ + 1, x, y))
+                return false;
+        }
+
+        fontGlyph.x_ = (short)x;
+        fontGlyph.y_ = (short)y;
+        fontGlyph.page_ = numPages - 1;
+
+        glyphMapping_[i->first_] = fontGlyph;
+    }
+
+    // Assume that format is the same for all textures and that bitmap font type may have more than one component
+    unsigned components = ConvertFormatToNumComponents(fontFace->textures_[0]->GetFormat());
+
+    // Save the existing textures as image resources
+    Vector<SharedPtr<Image> > oldImages;
+    for (unsigned i = 0; i < fontFace->textures_.Size(); ++i)
+        oldImages.Push(SaveFaceTexture(fontFace->textures_[i]));
+
+    Vector<SharedPtr<Image> > newImages(numPages);
+    for (unsigned i = 0; i < numPages; ++i)
+    {
+        SharedPtr<Image> image(new Image(font_->GetContext()));
+
+        int width = maxTextureSize;
+        int height = maxTextureSize;
+        if (i == numPages - 1)
+        {
+            width = allocator.GetWidth();
+            height = allocator.GetHeight();
+        }
+
+        image->SetSize(width, height, components);
+        memset(image->GetData(), 0, width * height * components);
+
+        newImages[i] = image;
+    }
+
+    for (HashMap<unsigned, Text3DFontGlyph>::Iterator i = glyphMapping_.Begin(); i != glyphMapping_.End(); ++i)
+    {
+        Text3DFontGlyph& newGlyph = i->second_;
+        const Text3DFontGlyph& oldGlyph = fontFace->glyphMapping_[i->first_];
+        Blit(newImages[newGlyph.page_], newGlyph.x_, newGlyph.y_, newGlyph.width_, newGlyph.height_, oldImages[oldGlyph.page_],
+            oldGlyph.x_, oldGlyph.y_, components);
+    }
+
+    textures_.Resize(newImages.Size());
+    for (unsigned i = 0; i < newImages.Size(); ++i)
+        textures_[i] = LoadFaceTexture(newImages[i]);
+
+    for (HashMap<unsigned, short>::ConstIterator i = fontFace->kerningMapping_.Begin(); i != fontFace->kerningMapping_.End(); ++i)
+    {
+        unsigned first = (i->first_) >> 16;
+        unsigned second = (i->first_) & 0xffff;
+        if (glyphMapping_.Find(first) != glyphMapping_.End() && glyphMapping_.Find(second) != glyphMapping_.End())
+            kerningMapping_[i->first_] = i->second_;
+    }
+
+    return true;
+}
+
+bool Text3DBitmap::Save(Serializer& dest, int pointSize, const String& indentation)
+{
+    Context* context = font_->GetContext();
+
+    SharedPtr<XMLFile> xml(new XMLFile(context));
+    XMLElement rootElem = xml->CreateRoot("font");
+
+    // Information
+    XMLElement childElem = rootElem.CreateChild("info");
+    String fileName = GetFileName(font_->GetName());
+    childElem.SetAttribute("face", fileName);
+    childElem.SetAttribute("size", String(pointSize));
+
+    // Common
+    childElem = rootElem.CreateChild("common");
+    childElem.SetInt("lineHeight", rowHeight_);
+    unsigned pages = textures_.Size();
+    childElem.SetUInt("pages", pages);
+
+    // Construct the path to store the texture
+    String pathName;
+    File* file = dynamic_cast<File*>(&dest);
+    if (file)
+        // If serialize to file, use the file's path
+        pathName = GetPath(file->GetName());
+    else
+        // Otherwise, use the font resource's path
+        pathName = "Data/" + GetPath(font_->GetName());
+
+    // Pages
+    childElem = rootElem.CreateChild("pages");
+    for (unsigned i = 0; i < pages; ++i)
+    {
+        XMLElement pageElem = childElem.CreateChild("page");
+        pageElem.SetInt("id", i);
+        String texFileName = fileName + "_" + String(i) + ".png";
+        pageElem.SetAttribute("file", texFileName);
+
+        // Save the font face texture to image file
+        SaveFaceTexture(textures_[i], pathName + texFileName);
+    }
+
+    // Chars and kernings
+    XMLElement charsElem = rootElem.CreateChild("chars");
+    unsigned numGlyphs = glyphMapping_.Size();
+    charsElem.SetInt("count", numGlyphs);
+
+    for (HashMap<unsigned, Text3DFontGlyph>::ConstIterator i = glyphMapping_.Begin(); i != glyphMapping_.End(); ++i)
+    {
+        // Char
+        XMLElement charElem = charsElem.CreateChild("char");
+        charElem.SetInt("id", i->first_);
+
+        const Text3DFontGlyph& glyph = i->second_;
+        charElem.SetInt("x", glyph.x_);
+        charElem.SetInt("y", glyph.y_);
+        charElem.SetInt("width", glyph.width_);
+        charElem.SetInt("height", glyph.height_);
+        charElem.SetInt("xoffset", glyph.offsetX_);
+        charElem.SetInt("yoffset", glyph.offsetY_);
+        charElem.SetInt("xadvance", glyph.advanceX_);
+        charElem.SetUInt("page", glyph.page_);
+    }
+
+    if (!kerningMapping_.Empty())
+    {
+        XMLElement kerningsElem = rootElem.CreateChild("kernings");
+        for (HashMap<unsigned, short>::ConstIterator i = kerningMapping_.Begin(); i != kerningMapping_.End(); ++i)
+        {
+            XMLElement kerningElem = kerningsElem.CreateChild("kerning");
+            kerningElem.SetInt("first", i->first_ >> 16);
+            kerningElem.SetInt("second", i->first_ & 0xffff);
+            kerningElem.SetInt("amount", i->second_);
+        }
+    }
+
+    return xml->Save(dest, indentation);
+}
+
+unsigned Text3DBitmap::ConvertFormatToNumComponents(unsigned format)
+{
+    if (format == Graphics::GetRGBAFormat())
+        return 4;
+    else if (format == Graphics::GetRGBFormat())
+        return 3;
+    else if (format == Graphics::GetLuminanceAlphaFormat())
+        return 2;
+    else
+        return 1;
+}
+
+SharedPtr<Image> Text3DBitmap::SaveFaceTexture(Texture2D* texture)
+{
+    SharedPtr<Image> image(new Image(font_->GetContext()));
+    image->SetSize(texture->GetWidth(), texture->GetHeight(), ConvertFormatToNumComponents(texture->GetFormat()));
+    if (!texture->GetData(0, image->GetData()))
+    {
+        ATOMIC_LOGERROR("Could not save texture to image resource");
+        return SharedPtr<Image>();
+    }
+    return image;
+}
+
+bool Text3DBitmap::SaveFaceTexture(Texture2D* texture, const String& fileName)
+{
+    SharedPtr<Image> image = SaveFaceTexture(texture);
+    return image ? image->SavePNG(fileName) : false;
+}
+
+void Text3DBitmap::Blit(Image* dest, int x, int y, int width, int height, Image* source, int sourceX, int sourceY, int components)
+{
+    unsigned char* destData = dest->GetData() + (y * dest->GetWidth() + x) * components;
+    unsigned char* sourceData = source->GetData() + (sourceY * source->GetWidth() + sourceX) * components;
+    for (int i = 0; i < height; ++i)
+    {
+        memcpy(destData, sourceData, (size_t)(width * components));
+        destData += dest->GetWidth() * components;
+        sourceData += source->GetWidth() * components;
+    }
+}
+
+}

+ 62 - 0
Source/Atomic/Graphics/Text3D/Text3DBitmap.h

@@ -0,0 +1,62 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "Text3DFontFace.h"
+
+namespace Atomic
+{
+
+class Image;
+class Serializer;
+
+/// Bitmap font face description.
+class ATOMIC_API Text3DBitmap : public Text3DFontFace
+{
+    ATOMIC_REFCOUNTED(Text3DBitmap)
+
+public:
+    /// Construct.
+    Text3DBitmap(Text3DFont* font);
+    /// Destruct.
+    ~Text3DBitmap();
+
+    /// Load font face.
+    virtual bool Load(const unsigned char* fontData, unsigned fontDataSize, int pointSize);
+    /// Load from existed font face, pack used glyphs into smallest texture size and smallest number of texture.
+    bool Load(Text3DFontFace* fontFace, bool usedGlyphs);
+    /// Save as a new bitmap font type in XML format. Return true if successful.
+    bool Save(Serializer& dest, int pointSize, const String& indentation = "\t");
+
+private:
+    /// Convert graphics format to number of components.
+    unsigned ConvertFormatToNumComponents(unsigned format);
+    /// Save font face texture as image resource.
+    SharedPtr<Image> SaveFaceTexture(Texture2D* texture);
+    /// Save font face texture as image file.
+    bool SaveFaceTexture(Texture2D* texture, const String& fileName);
+    /// Blit.
+    void Blit(Image* dest, int x, int y, int width, int height, Image* source, int sourceX, int sourceY, int components);
+};
+
+}

+ 231 - 0
Source/Atomic/Graphics/Text3D/Text3DFont.cpp

@@ -0,0 +1,231 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../../Core/Context.h"
+#include "../../Core/Profiler.h"
+#include "../Graphics.h"
+#include "../../IO/Deserializer.h"
+#include "../../IO/FileSystem.h"
+#include "Text3DFont.h"
+#include "Text3DBitmap.h"
+#include "Text3DFreeType.h"
+#include "../../Resource/ResourceCache.h"
+#include "../../Resource/XMLElement.h"
+#include "../../Resource/XMLFile.h"
+#include "../../DebugNew.h"
+
+namespace Atomic
+{
+
+static const int MIN_POINT_SIZE = 1;
+static const int MAX_POINT_SIZE = 96;
+
+Text3DFont::Text3DFont(Context* context) :
+    Resource(context),
+    fontDataSize_(0),
+    absoluteOffset_(IntVector2::ZERO),
+    scaledOffset_(Vector2::ZERO),
+    fontType_(FONT_NONE),
+    sdfFont_(false)
+{
+}
+
+Text3DFont::~Text3DFont()
+{
+    // To ensure FreeType deallocates properly, first clear all faces, then release the raw font data
+    ReleaseFaces();
+    fontData_.Reset();
+}
+
+void Text3DFont::RegisterObject(Context* context)
+{
+    context->RegisterFactory<Text3DFont>();
+}
+
+bool Text3DFont::BeginLoad(Deserializer& source)
+{
+    // In headless mode, do not actually load, just return success
+    Graphics* graphics = GetSubsystem<Graphics>();
+    if (!graphics)
+        return true;
+
+    fontType_ = FONT_NONE;
+    faces_.Clear();
+
+    fontDataSize_ = source.GetSize();
+    if (fontDataSize_)
+    {
+        fontData_ = new unsigned char[fontDataSize_];
+        if (source.Read(&fontData_[0], fontDataSize_) != fontDataSize_)
+            return false;
+    }
+    else
+    {
+        fontData_.Reset();
+        return false;
+    }
+
+    String ext = GetExtension(GetName());
+    if (ext == ".ttf" || ext == ".otf" || ext == ".woff")
+    {
+        fontType_ = FONT_FREETYPE;
+        LoadParameters();
+    }
+    else if (ext == ".xml" || ext == ".fnt" || ext == ".sdf")
+        fontType_ = FONT_BITMAP;
+
+    sdfFont_ = ext == ".sdf";
+
+    SetMemoryUse(fontDataSize_);
+    return true;
+}
+
+bool Text3DFont::SaveXML(Serializer& dest, int pointSize, bool usedGlyphs, const String& indentation)
+{
+    Text3DFontFace* fontFace = GetFace(pointSize);
+    if (!fontFace)
+        return false;
+
+    ATOMIC_PROFILE(FontSaveXML);
+
+    SharedPtr<Text3DBitmap> packedFontFace(new Text3DBitmap(this));
+    if (!packedFontFace->Load(fontFace, usedGlyphs))
+        return false;
+
+    return packedFontFace->Save(dest, pointSize, indentation);
+}
+
+void Text3DFont::SetAbsoluteGlyphOffset(const IntVector2& offset)
+{
+    absoluteOffset_ = offset;
+}
+
+void Text3DFont::SetScaledGlyphOffset(const Vector2& offset)
+{
+    scaledOffset_ = offset;
+}
+
+Text3DFontFace* Text3DFont::GetFace(int pointSize)
+{
+    // In headless mode, always return null
+    Graphics* graphics = GetSubsystem<Graphics>();
+    if (!graphics)
+        return 0;
+
+    // For bitmap font type, always return the same font face provided by the font's bitmap file regardless of the actual requested point size
+    if (fontType_ == FONT_BITMAP)
+        pointSize = 0;
+    else
+        pointSize = Clamp(pointSize, MIN_POINT_SIZE, MAX_POINT_SIZE);
+
+    HashMap<int, SharedPtr<Text3DFontFace> >::Iterator i = faces_.Find(pointSize);
+    if (i != faces_.End())
+    {
+        if (!i->second_->IsDataLost())
+            return i->second_;
+        else
+        {
+            // Erase and reload face if texture data lost (OpenGL mode only)
+            faces_.Erase(i);
+        }
+    }
+
+    ATOMIC_PROFILE(GetFontFace);
+
+    switch (fontType_)
+    {
+    case FONT_FREETYPE:
+        return GetFaceFreeType(pointSize);
+
+    case FONT_BITMAP:
+        return GetFaceBitmap(pointSize);
+
+    default:
+        return 0;
+    }
+}
+
+IntVector2 Text3DFont::GetTotalGlyphOffset(int pointSize) const
+{
+    Vector2 multipliedOffset = (float)pointSize * scaledOffset_;
+    return absoluteOffset_ + IntVector2((int)multipliedOffset.x_, (int)multipliedOffset.y_);
+}
+
+void Text3DFont::ReleaseFaces()
+{
+    faces_.Clear();
+}
+
+void Text3DFont::LoadParameters()
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    String xmlName = ReplaceExtension(GetName(), ".xml");
+    SharedPtr<XMLFile> xml = cache->GetTempResource<XMLFile>(xmlName, false);
+    if (!xml)
+        return;
+
+    XMLElement rootElem = xml->GetRoot();
+
+    XMLElement absoluteElem = rootElem.GetChild("absoluteoffset");
+    if (!absoluteElem)
+        absoluteElem = rootElem.GetChild("absolute");
+
+    if (absoluteElem)
+    {
+        absoluteOffset_.x_ = absoluteElem.GetInt("x");
+        absoluteOffset_.y_ = absoluteElem.GetInt("y");
+    }
+
+    XMLElement scaledElem = rootElem.GetChild("scaledoffset");
+    if (!scaledElem)
+        scaledElem = rootElem.GetChild("scaled");
+
+    if (scaledElem)
+    {
+        scaledOffset_.x_ = scaledElem.GetFloat("x");
+        scaledOffset_.y_ = scaledElem.GetFloat("y");
+    }
+}
+
+Text3DFontFace* Text3DFont::GetFaceFreeType(int pointSize)
+{
+    SharedPtr<Text3DFontFace> newFace(new Text3DFreeType(this));
+    if (!newFace->Load(&fontData_[0], fontDataSize_, pointSize))
+        return 0;
+
+    faces_[pointSize] = newFace;
+    return newFace;
+}
+
+Text3DFontFace* Text3DFont::GetFaceBitmap(int pointSize)
+{
+    SharedPtr<Text3DFontFace> newFace(new Text3DBitmap(this));
+    if (!newFace->Load(&fontData_[0], fontDataSize_, pointSize))
+        return 0;
+
+    faces_[pointSize] = newFace;
+    return newFace;
+}
+
+}

+ 113 - 0
Source/Atomic/Graphics/Text3D/Text3DFont.h

@@ -0,0 +1,113 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../../Container/ArrayPtr.h"
+#include "../../Resource/Resource.h"
+
+namespace Atomic
+{
+
+class Text3DFontFace;
+
+static const int TEXT3D_FONT_TEXTURE_MIN_SIZE = 128;
+static const int TEXT3D_DEFAULT_FONT_TEXTURE_MAX_SIZE = 2048;
+static const int TEXT3D_FONT_DPI = 96;
+
+/// %Font file type.
+enum Text3DFontType
+{
+    FONT_NONE = 0,
+    FONT_FREETYPE,
+    FONT_BITMAP,
+    MAX_FONT_TYPES
+};
+
+/// %Font resource.
+class ATOMIC_API Text3DFont : public Resource
+{
+    ATOMIC_OBJECT(Text3DFont, Resource)
+
+public:
+    /// Construct.
+    Text3DFont(Context* context);
+    /// Destruct.
+    virtual ~Text3DFont();
+    /// Register object factory.
+    static void RegisterObject(Context* context);
+
+    /// Load resource from stream. May be called from a worker thread. Return true if successful.
+    virtual bool BeginLoad(Deserializer& source);
+    /// Save resource as a new bitmap font type in XML format. Return true if successful.
+    bool SaveXML(Serializer& dest, int pointSize, bool usedGlyphs = false, const String& indentation = "\t");
+    /// Set absolute (in pixels) position adjustment for glyphs.
+    void SetAbsoluteGlyphOffset(const IntVector2& offset);
+    /// Set point size scaled position adjustment for glyphs.
+    void SetScaledGlyphOffset(const Vector2& offset);
+
+    /// Return font face. Pack and render to a texture if not rendered yet. Return null on error.
+    Text3DFontFace* GetFace(int pointSize);
+
+    /// Return font type.
+    Text3DFontType GetFontType() const { return fontType_; }
+
+    /// Is signed distance field font.
+    bool IsSDFFont() const { return sdfFont_; }
+
+    /// Return absolute position adjustment for glyphs.
+    const IntVector2& GetAbsoluteGlyphOffset() const { return absoluteOffset_; }
+
+    /// Return point size scaled position adjustment for glyphs.
+    const Vector2& GetScaledGlyphOffset() const { return scaledOffset_; }
+
+    /// Return the total effective offset for a point size.
+    IntVector2 GetTotalGlyphOffset(int pointSize) const;
+
+    /// Release font faces and recreate them next time when requested. Called when font textures lost or global font properties change.
+    void ReleaseFaces();
+
+private:
+    /// Load font glyph offset parameters from an optional XML file. Called internally when loading TrueType fonts.
+    void LoadParameters();
+    /// Return font face using FreeType. Called internally. Return null on error.
+    Text3DFontFace* GetFaceFreeType(int pointSize);
+    /// Return bitmap font face. Called internally. Return null on error.
+    Text3DFontFace* GetFaceBitmap(int pointSize);
+
+    /// Created faces.
+    HashMap<int, SharedPtr<Text3DFontFace> > faces_;
+    /// Font data.
+    SharedArrayPtr<unsigned char> fontData_;
+    /// Size of font data.
+    unsigned fontDataSize_;
+    /// Absolute position adjustment for glyphs.
+    IntVector2 absoluteOffset_;
+    /// Point size scaled position adjustment for glyphs.
+    Vector2 scaledOffset_;
+    /// Font type.
+    Text3DFontType fontType_;
+    /// Signed distance field font flag.
+    bool sdfFont_;
+};
+
+}

+ 126 - 0
Source/Atomic/Graphics/Text3D/Text3DFontFace.cpp

@@ -0,0 +1,126 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../../Core/Context.h"
+#include "../../IO/Log.h"
+#include "../Texture2D.h"
+#include "../../Resource/Image.h"
+#include "Text3DFont.h"
+#include "Text3DFontFace.h"
+
+#include "../../DebugNew.h"
+
+namespace Atomic
+{
+
+Text3DFontGlyph::Text3DFontGlyph() :
+    page_(M_MAX_UNSIGNED),
+    used_(false)
+{
+}
+
+Text3DFontFace::Text3DFontFace(Text3DFont* font) :
+    font_(font)
+{
+}
+
+Text3DFontFace::~Text3DFontFace()
+{
+    if (font_)
+    {
+        // When a face is unloaded, deduct the used texture data size from the parent font
+        unsigned totalTextureSize = 0;
+        for (unsigned i = 0; i < textures_.Size(); ++i)
+            totalTextureSize += textures_[i]->GetWidth() * textures_[i]->GetHeight();
+        font_->SetMemoryUse(font_->GetMemoryUse() - totalTextureSize);
+    }
+}
+
+const Text3DFontGlyph* Text3DFontFace::GetGlyph(unsigned c)
+{
+    HashMap<unsigned, Text3DFontGlyph>::Iterator i = glyphMapping_.Find(c);
+    if (i != glyphMapping_.End())
+    {
+        Text3DFontGlyph& glyph = i->second_;
+        glyph.used_ = true;
+        return &glyph;
+    }
+    else
+        return 0;
+}
+
+short Text3DFontFace::GetKerning(unsigned c, unsigned d) const
+{
+    if (kerningMapping_.Empty())
+        return 0;
+
+    if (c == '\n' || d == '\n')
+        return 0;
+
+    if (c > 0xffff || d > 0xffff)
+        return 0;
+
+    unsigned value = (c << 16) + d;
+
+    HashMap<unsigned, short>::ConstIterator i = kerningMapping_.Find(value);
+    if (i != kerningMapping_.End())
+        return i->second_;
+
+    return 0;
+}
+
+bool Text3DFontFace::IsDataLost() const
+{
+    for (unsigned i = 0; i < textures_.Size(); ++i)
+    {
+        if (textures_[i]->IsDataLost())
+            return true;
+    }
+    return false;
+}
+
+
+SharedPtr<Texture2D> Text3DFontFace::CreateFaceTexture()
+{
+    SharedPtr<Texture2D> texture(new Texture2D(font_->GetContext()));
+    texture->SetMipsToSkip(QUALITY_LOW, 0); // No quality reduction
+    texture->SetNumLevels(1); // No mipmaps
+    texture->SetAddressMode(COORD_U, ADDRESS_BORDER);
+    texture->SetAddressMode(COORD_V, ADDRESS_BORDER);
+    texture->SetBorderColor(Color(0.0f, 0.0f, 0.0f, 0.0f));
+    return texture;
+}
+
+SharedPtr<Texture2D> Text3DFontFace::LoadFaceTexture(SharedPtr<Image> image)
+{
+    SharedPtr<Texture2D> texture = CreateFaceTexture();
+    if (!texture->SetData(image, true))
+    {
+        ATOMIC_LOGERROR("Could not load texture from image resource");
+        return SharedPtr<Texture2D>();
+    }
+    return texture;
+}
+
+}

+ 118 - 0
Source/Atomic/Graphics/Text3D/Text3DFontFace.h

@@ -0,0 +1,118 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../../Container/ArrayPtr.h"
+#include "../../Container/List.h"
+#include "../../Math/AreaAllocator.h"
+
+namespace Atomic
+{
+
+class Text3DFont;
+class Image;
+class Texture2D;
+
+/// %Font glyph description.
+struct ATOMIC_API Text3DFontGlyph
+{
+    /// Construct.
+    Text3DFontGlyph();
+
+    /// X position in texture.
+    short x_;
+    /// Y position in texture.
+    short y_;
+    /// Width.
+    short width_;
+    /// Height.
+    short height_;
+    /// Glyph X offset from origin.
+    short offsetX_;
+    /// Glyph Y offset from origin.
+    short offsetY_;
+    /// Horizontal advance.
+    short advanceX_;
+    /// Texture page. M_MAX_UNSIGNED if not yet resident on any texture.
+    unsigned page_;
+    /// Used flag.
+    bool used_;
+};
+
+/// %Font face description.
+class ATOMIC_API Text3DFontFace : public RefCounted
+{
+    friend class Text3DFont;
+
+    ATOMIC_REFCOUNTED(FontFace)
+
+public:
+    /// Construct.
+    Text3DFontFace(Text3DFont* font);
+    /// Destruct.
+    ~Text3DFontFace();
+
+    /// Load font face.
+    virtual bool Load(const unsigned char* fontData, unsigned fontDataSize, int pointSize) = 0;
+    /// Return pointer to the glyph structure corresponding to a character. Return null if glyph not found.
+    virtual const Text3DFontGlyph* GetGlyph(unsigned c);
+
+    /// Return if font face uses mutable glyphs.
+    virtual bool HasMutableGlyphs() const { return false; }
+
+    /// Return the kerning for a character and the next character.
+    short GetKerning(unsigned c, unsigned d) const;
+    /// Return true when one of the texture has a data loss.
+    bool IsDataLost() const;
+
+    /// Return point size.
+    int GetPointSize() const { return pointSize_; }
+
+    /// Return row height.
+    int GetRowHeight() const { return rowHeight_; }
+
+    /// Return textures.
+    const Vector<SharedPtr<Texture2D> >& GetTextures() const { return textures_; }
+
+protected:
+    friend class Text3DBitmap;
+    /// Create a texture for font rendering.
+    SharedPtr<Texture2D> CreateFaceTexture();
+    /// Load font face texture from image resource.
+    SharedPtr<Texture2D> LoadFaceTexture(SharedPtr<Image> image);
+
+    /// Parent font.
+    Text3DFont* font_;
+    /// Glyph mapping.
+    HashMap<unsigned, Text3DFontGlyph> glyphMapping_;
+    /// Kerning mapping.
+    HashMap<unsigned, short> kerningMapping_;
+    /// Glyph texture pages.
+    Vector<SharedPtr<Texture2D> > textures_;
+    /// Point size.
+    int pointSize_;
+    /// Row height.
+    int rowHeight_;
+};
+
+}

+ 444 - 0
Source/Atomic/Graphics/Text3D/Text3DFreeType.cpp

@@ -0,0 +1,444 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../../Core/Context.h"
+#include "../Graphics.h"
+#include "../Texture2D.h"
+#include "../../IO/FileSystem.h"
+#include "../../IO/Log.h"
+#include "../../IO/MemoryBuffer.h"
+#include "Text3DFont.h"
+#include "Text3DFreeType.h"
+
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include FT_TRUETYPE_TABLES_H
+
+#include "../../DebugNew.h"
+
+namespace Atomic
+{
+
+inline int RoundToPixels(FT_Pos value)
+{
+    return (int)(value >> 6) + (((value & 0x3f) >= 0x20) ? 1 : 0);
+}
+
+/// FreeType library subsystem.
+class FreeTypeLibrary : public Object
+{
+    ATOMIC_OBJECT(FreeTypeLibrary, Object);
+
+public:
+    /// Construct.
+    FreeTypeLibrary(Context* context) :
+        Object(context)
+    {
+        FT_Error error = FT_Init_FreeType(&library_);
+        if (error)
+            ATOMIC_LOGERROR("Could not initialize FreeType library");
+    }
+
+    /// Destruct.
+    virtual ~FreeTypeLibrary()
+    {
+        FT_Done_FreeType(library_);
+    }
+
+    FT_Library GetLibrary() const { return library_; }
+
+private:
+    /// FreeType library.
+    FT_Library library_;
+};
+
+Text3DFreeType::Text3DFreeType(Text3DFont* font) :
+    Text3DFontFace(font),
+    face_(0),
+    loadMode_(FT_LOAD_DEFAULT),
+    hasMutableGlyph_(false),
+    forceAutoHint_(false)
+{
+}
+
+Text3DFreeType::~Text3DFreeType()
+{
+    if (face_)
+    {
+        FT_Done_Face((FT_Face)face_);
+        face_ = 0;
+    }
+}
+
+bool Text3DFreeType::Load(const unsigned char* fontData, unsigned fontDataSize, int pointSize)
+{
+    Context* context = font_->GetContext();
+
+    // Create & initialize FreeType library if it does not exist yet
+    FreeTypeLibrary* freeType = font_->GetSubsystem<FreeTypeLibrary>();
+    if (!freeType)
+        context->RegisterSubsystem(freeType = new FreeTypeLibrary(context));
+
+    // Ensure the FreeType library is kept alive as long as TTF font resources exist
+    freeType_ = freeType;
+
+    int maxTextureSize = TEXT3D_DEFAULT_FONT_TEXTURE_MAX_SIZE;
+
+    FT_Face face;
+    FT_Error error;
+    FT_Library library = freeType->GetLibrary();
+
+    if (pointSize <= 0)
+    {
+        ATOMIC_LOGERROR("Zero or negative point size");
+        return false;
+    }
+
+    if (!fontDataSize)
+    {
+        ATOMIC_LOGERROR("Could not create font face from zero size data");
+        return false;
+    }
+
+    error = FT_New_Memory_Face(library, fontData, fontDataSize, 0, &face);
+    if (error)
+    {
+        ATOMIC_LOGERROR("Could not create font face");
+        return false;
+    }
+    error = FT_Set_Char_Size(face, 0, pointSize * 64, TEXT3D_FONT_DPI, TEXT3D_FONT_DPI);
+    if (error)
+    {
+        FT_Done_Face(face);
+        ATOMIC_LOGERROR("Could not set font point size " + String(pointSize));
+        return false;
+    }
+
+    face_ = face;
+
+    unsigned numGlyphs = (unsigned)face->num_glyphs;
+    ATOMIC_LOGDEBUGF("Font face %s (%dpt) has %d glyphs", GetFileName(font_->GetName()).CString(), pointSize, numGlyphs);
+
+    PODVector<unsigned> charCodes(numGlyphs + 1, 0);
+
+    // Attempt to load space glyph first regardless if it's listed or not
+    // In some fonts (Consola) it is missing
+    charCodes[0] = 32;
+
+    FT_UInt glyphIndex;
+    FT_ULong charCode = FT_Get_First_Char(face, &glyphIndex);
+    while (glyphIndex != 0)
+    {
+        if (glyphIndex < numGlyphs)
+            charCodes[glyphIndex + 1] = (unsigned)charCode;
+
+        charCode = FT_Get_Next_Char(face, charCode, &glyphIndex);
+    }
+
+    // Load each of the glyphs to see the sizes & store other information
+    loadMode_ = (int)(forceAutoHint_ ? FT_LOAD_FORCE_AUTOHINT : FT_LOAD_DEFAULT);
+    ascender_ = RoundToPixels(face->size->metrics.ascender);
+    rowHeight_ = RoundToPixels(face->size->metrics.height);
+    pointSize_ = pointSize;
+
+    // Check if the font's OS/2 info gives different (larger) values for ascender & descender
+    TT_OS2* os2Info = (TT_OS2*)FT_Get_Sfnt_Table(face, ft_sfnt_os2);
+    if (os2Info)
+    {
+        int descender = RoundToPixels(face->size->metrics.descender);
+        ascender_ = Max(ascender_, os2Info->usWinAscent * face->size->metrics.y_ppem / face->units_per_EM);
+        ascender_ = Max(ascender_, os2Info->sTypoAscender * face->size->metrics.y_ppem / face->units_per_EM);
+        descender = Max(descender, os2Info->usWinDescent * face->size->metrics.y_ppem / face->units_per_EM);
+        descender = Max(descender, os2Info->sTypoDescender * face->size->metrics.y_ppem / face->units_per_EM);
+        rowHeight_ = Max(rowHeight_, ascender_ + descender);
+    }
+
+    int textureWidth = maxTextureSize;
+    int textureHeight = maxTextureSize;
+    hasMutableGlyph_ = false;
+
+    SharedPtr<Image> image(new Image(font_->GetContext()));
+    image->SetSize(textureWidth, textureHeight, 1);
+    unsigned char* imageData = image->GetData();
+    memset(imageData, 0, (size_t)(image->GetWidth() * image->GetHeight()));
+    allocator_.Reset(TEXT3D_FONT_TEXTURE_MIN_SIZE, TEXT3D_FONT_TEXTURE_MIN_SIZE, textureWidth, textureHeight);
+
+    for (unsigned i = 0; i < charCodes.Size(); ++i)
+    {
+        unsigned charCode = charCodes[i];
+        if (charCode == 0)
+            continue;
+
+        if (!LoadCharGlyph(charCode, image))
+        {
+            hasMutableGlyph_ = true;
+            break;
+        }
+    }
+
+    SharedPtr<Texture2D> texture = LoadFaceTexture(image);
+    if (!texture)
+        return false;
+
+    textures_.Push(texture);
+    font_->SetMemoryUse(font_->GetMemoryUse() + textureWidth * textureHeight);
+
+    // Store kerning if face has kerning information
+    if (FT_HAS_KERNING(face))
+    {
+        // Read kerning manually to be more efficient and avoid out of memory crash when use large font file, for example there
+        // are 29354 glyphs in msyh.ttf
+        FT_ULong tagKern = FT_MAKE_TAG('k', 'e', 'r', 'n');
+        FT_ULong kerningTableSize = 0;
+        FT_Error error = FT_Load_Sfnt_Table(face, tagKern, 0, 0, &kerningTableSize);
+        if (error)
+        {
+            ATOMIC_LOGERROR("Could not get kerning table length");
+            return false;
+        }
+
+        SharedArrayPtr<unsigned char> kerningTable(new unsigned char[kerningTableSize]);
+        error = FT_Load_Sfnt_Table(face, tagKern, 0, kerningTable, &kerningTableSize);
+        if (error)
+        {
+            ATOMIC_LOGERROR("Could not load kerning table");
+            return false;
+        }
+
+        // Convert big endian to little endian
+        for (unsigned i = 0; i < kerningTableSize; i += 2)
+            Swap(kerningTable[i], kerningTable[i + 1]);
+        MemoryBuffer deserializer(kerningTable, (unsigned)kerningTableSize);
+
+        unsigned short version = deserializer.ReadUShort();
+        if (version == 0)
+        {
+            unsigned numKerningTables = deserializer.ReadUShort();
+            for (unsigned i = 0; i < numKerningTables; ++i)
+            {
+                unsigned short version = deserializer.ReadUShort();
+                unsigned short length = deserializer.ReadUShort();
+                unsigned short coverage = deserializer.ReadUShort();
+
+                if (version == 0 && coverage == 1)
+                {
+                    unsigned numKerningPairs = deserializer.ReadUShort();
+                    // Skip searchRange, entrySelector and rangeShift
+                    deserializer.Seek((unsigned)(deserializer.GetPosition() + 3 * sizeof(unsigned short)));
+
+                    for (unsigned j = 0; j < numKerningPairs; ++j)
+                    {
+                        unsigned leftIndex = deserializer.ReadUShort();
+                        unsigned rightIndex = deserializer.ReadUShort();
+                        short amount = RoundToPixels(deserializer.ReadShort());
+
+                        unsigned leftCharCode = leftIndex < numGlyphs ? charCodes[leftIndex] : 0;
+                        unsigned rightCharCode = rightIndex < numGlyphs ? charCodes[rightIndex] : 0;
+                        if (leftCharCode != 0 && rightCharCode != 0)
+                        {
+                            unsigned value = (leftCharCode << 16) + rightCharCode;
+                            kerningMapping_[value] = amount;
+                        }
+                    }
+                }
+                else
+                {
+                    // Kerning table contains information we do not support; skip and move to the next (length includes header)
+                    deserializer.Seek((unsigned)(deserializer.GetPosition() + length - 3 * sizeof(unsigned short)));
+                }
+            }
+        }
+        else
+            ATOMIC_LOGWARNING("Can not read kerning information: not version 0");
+    }
+
+    if (!hasMutableGlyph_)
+    {
+        FT_Done_Face(face);
+        face_ = 0;
+    }
+
+    return true;
+}
+
+const Text3DFontGlyph* Text3DFreeType::GetGlyph(unsigned c)
+{
+    HashMap<unsigned, Text3DFontGlyph>::Iterator i = glyphMapping_.Find(c);
+    if (i != glyphMapping_.End())
+    {
+        Text3DFontGlyph& glyph = i->second_;
+        glyph.used_ = true;
+        return &glyph;
+    }
+
+    if (LoadCharGlyph(c))
+    {
+        HashMap<unsigned, Text3DFontGlyph>::Iterator i = glyphMapping_.Find(c);
+        if (i != glyphMapping_.End())
+        {
+            Text3DFontGlyph& glyph = i->second_;
+            glyph.used_ = true;
+            return &glyph;
+        }
+    }
+
+    return 0;
+}
+
+bool Text3DFreeType::SetupNextTexture(int textureWidth, int textureHeight)
+{
+    SharedPtr<Image> image(new Image(font_->GetContext()));
+    image->SetSize(textureWidth, textureHeight, 1);
+    unsigned char* imageData = image->GetData();
+    memset(imageData, 0, (size_t)(image->GetWidth() * image->GetHeight()));
+
+    SharedPtr<Texture2D> texture = LoadFaceTexture(image);
+    if (!texture)
+        return false;
+
+    textures_.Push(texture);
+    allocator_.Reset(TEXT3D_FONT_TEXTURE_MIN_SIZE, TEXT3D_FONT_TEXTURE_MIN_SIZE, textureWidth, textureHeight);
+
+    font_->SetMemoryUse(font_->GetMemoryUse() + textureWidth * textureHeight);
+
+    return true;
+}
+
+bool Text3DFreeType::LoadCharGlyph(unsigned charCode, Image* image)
+{
+    if (!face_)
+        return false;
+
+    FT_Face face = (FT_Face)face_;
+    FT_GlyphSlot slot = face->glyph;
+
+    Text3DFontGlyph fontGlyph;
+    FT_Error error = FT_Load_Char(face, charCode, loadMode_ | FT_LOAD_RENDER);
+    if (error)
+    {
+        const char* family = face->family_name ? face->family_name : "NULL";
+        ATOMIC_LOGERRORF("FT_Load_Char failed (family: %s, char code: %u)", family, charCode);
+        fontGlyph.width_ = 0;
+        fontGlyph.height_ = 0;
+        fontGlyph.offsetX_ = 0;
+        fontGlyph.offsetY_ = 0;
+        fontGlyph.advanceX_ = 0;
+        fontGlyph.page_ = 0;
+    }
+    else
+    {
+        // Note: position within texture will be filled later
+        fontGlyph.width_ = slot->bitmap.width;
+        fontGlyph.height_ = slot->bitmap.rows;
+        fontGlyph.offsetX_ = slot->bitmap_left;
+        fontGlyph.offsetY_ = ascender_ - slot->bitmap_top;
+        fontGlyph.advanceX_ = (short)RoundToPixels(slot->metrics.horiAdvance);
+    }
+
+    int x = 0, y = 0;
+    if (fontGlyph.width_ > 0 && fontGlyph.height_ > 0)
+    {
+        if (!allocator_.Allocate(fontGlyph.width_ + 1, fontGlyph.height_ + 1, x, y))
+        {
+            if (image)
+            {
+                // We're rendering into a fixed image and we ran out of room.
+                return false;
+            }
+
+            int w = allocator_.GetWidth();
+            int h = allocator_.GetHeight();
+            if (!SetupNextTexture(w, h))
+            {
+                ATOMIC_LOGWARNINGF("Text3DFreeType::LoadCharGlyph: failed to allocate new %dx%d texture", w, h);
+                return false;
+            }
+
+            if (!allocator_.Allocate(fontGlyph.width_ + 1, fontGlyph.height_ + 1, x, y))
+            {
+                ATOMIC_LOGWARNINGF("Text3DFreeType::LoadCharGlyph: failed to position char code %u in blank page", charCode);
+                return false;
+            }
+        }
+
+        fontGlyph.x_ = (short)x;
+        fontGlyph.y_ = (short)y;
+
+        unsigned char* dest = 0;
+        unsigned pitch = 0;
+        if (image)
+        {
+            fontGlyph.page_ = 0;
+            dest = image->GetData() + fontGlyph.y_ * image->GetWidth() + fontGlyph.x_;
+            pitch = (unsigned)image->GetWidth();
+        }
+        else
+        {
+            fontGlyph.page_ = textures_.Size() - 1;
+            dest = new unsigned char[fontGlyph.width_ * fontGlyph.height_];
+            pitch = (unsigned)fontGlyph.width_;
+        }
+
+        if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO)
+        {
+            for (unsigned y = 0; y < (unsigned)slot->bitmap.rows; ++y)
+            {
+                unsigned char* src = slot->bitmap.buffer + slot->bitmap.pitch * y;
+                unsigned char* rowDest = dest + y * pitch;
+
+                for (unsigned x = 0; x < (unsigned)slot->bitmap.width; ++x)
+                    rowDest[x] = (unsigned char)((src[x >> 3] & (0x80 >> (x & 7))) ? 255 : 0);
+            }
+        }
+        else
+        {
+            for (unsigned y = 0; y < (unsigned)slot->bitmap.rows; ++y)
+            {
+                unsigned char* src = slot->bitmap.buffer + slot->bitmap.pitch * y;
+                unsigned char* rowDest = dest + y * pitch;
+
+                for (unsigned x = 0; x < (unsigned)slot->bitmap.width; ++x)
+                    rowDest[x] = src[x];
+            }
+        }
+
+        if (!image)
+        {
+            textures_.Back()->SetData(0, fontGlyph.x_, fontGlyph.y_, fontGlyph.width_, fontGlyph.height_, dest);
+            delete[] dest;
+        }
+    }
+    else
+    {
+        fontGlyph.x_ = 0;
+        fontGlyph.y_ = 0;
+        fontGlyph.page_ = 0;
+    }
+
+    glyphMapping_[charCode] = fontGlyph;
+
+    return true;
+}
+
+}

+ 77 - 0
Source/Atomic/Graphics/Text3D/Text3DFreeType.h

@@ -0,0 +1,77 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "Text3DFontFace.h"
+
+namespace Atomic
+{
+
+class FreeTypeLibrary;
+class Texture2D;
+
+/// Free type font face description.
+class ATOMIC_API Text3DFreeType : public Text3DFontFace
+{
+    ATOMIC_REFCOUNTED(Text3DFreeType)
+
+public:
+    /// Construct.
+    Text3DFreeType(Text3DFont* font);
+    /// Destruct.
+    ~Text3DFreeType();
+
+    /// Load font face.
+    virtual bool Load(const unsigned char* fontData, unsigned fontDataSize, int pointSize);
+    /// Return pointer to the glyph structure corresponding to a character. Return null if glyph not found.
+    virtual const Text3DFontGlyph* GetGlyph(unsigned c);
+
+    /// Return if font face uses mutable glyphs.
+    virtual bool HasMutableGlyphs() const { return hasMutableGlyph_; }
+
+    /// Set whether to force font autohinting instead of using FreeType's TTF bytecode interpreter.
+    void SetForceAutoHint(bool enable) { forceAutoHint_ = enable; }
+
+private:
+    /// Setup next texture.
+    bool SetupNextTexture(int textureWidth, int textureHeight);
+    /// Load char glyph.
+    bool LoadCharGlyph(unsigned charCode, Image* image = 0);
+
+    /// FreeType library.
+    SharedPtr<FreeTypeLibrary> freeType_;
+    /// FreeType face. Non-null after creation only in dynamic mode.
+    void* face_;
+    /// Load mode.
+    int loadMode_;
+    /// Ascender.
+    int ascender_;
+    /// Has mutable glyph.
+    bool hasMutableGlyph_;
+    /// Glyph area allocator.
+    AreaAllocator allocator_;
+
+    bool forceAutoHint_;
+};
+
+}

+ 997 - 0
Source/Atomic/Graphics/Text3D/Text3DText.cpp

@@ -0,0 +1,997 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../../Precompiled.h"
+
+#include "../../Core/Context.h"
+#include "../../Core/Profiler.h"
+#include "../../Core/CoreEvents.h"
+#include "../Texture2D.h"
+#include "../../IO/Log.h"
+#include "../../Resource/ResourceCache.h"
+#include "../../Resource/Localization.h"
+#include "../../Resource/ResourceEvents.h"
+
+#include "Text3DFont.h"
+#include "Text3DFontFace.h"
+#include "Text3DText.h"
+
+#include "../../DebugNew.h"
+
+namespace Atomic
+{
+
+const char* textEffects[] =
+{
+    "None",
+    "Shadow",
+    "Stroke",
+    0
+};
+
+const char* horizontalAlignments[] =
+{
+    "Left",
+    "Center",
+    "Right",
+    0
+};
+
+const char* verticalAlignments[] =
+{
+    "Top",
+    "Center",
+    "Bottom",
+    0
+};
+
+static const float MIN_ROW_SPACING = 0.5f;
+
+extern const char* horizontalAlignments[];
+extern const char* GEOMETRY_CATEGORY;
+
+Text3DText::Text3DText(Context* context) :
+    Animatable(context),
+    fontSize_(TEXT3D_DEFAULT_FONT_SIZE),
+    textAlignment_(HA_LEFT),
+    rowSpacing_(1.0f),
+    wordWrap_(false),
+    autoLocalizable_(false),
+    charLocationsDirty_(true),
+    selectionStart_(0),
+    selectionLength_(0),
+    selectionColor_(Color::TRANSPARENT),
+    hoverColor_(Color::TRANSPARENT),
+    textEffect_(TE_NONE),
+    shadowOffset_(IntVector2(1, 1)),
+    strokeThickness_(1),
+    roundStroke_(false),
+    effectColor_(Color::BLACK),
+    effectDepthBias_(0.0f),
+    rowHeight_(0),
+    indent_(0),
+    indentSpacing_(16),
+    colorGradient_(false),
+    opacity_(1.0f),
+    hovering_(false),
+    selected_(false)
+{
+}
+
+Text3DText::~Text3DText()
+{
+}
+
+void Text3DText::RegisterObject(Context* context)
+{
+    context->RegisterFactory<Text3DText>(GEOMETRY_CATEGORY);
+
+    ATOMIC_COPY_BASE_ATTRIBUTES(Animatable);
+    ATOMIC_UPDATE_ATTRIBUTE_DEFAULT_VALUE("Use Derived Opacity", false);
+    ATOMIC_MIXED_ACCESSOR_ATTRIBUTE("Font", GetFontAttr, SetFontAttr, ResourceRef, ResourceRef(Text3DFont::GetTypeStatic()), AM_FILE);
+    ATOMIC_ATTRIBUTE("Font Size", int, fontSize_, TEXT3D_DEFAULT_FONT_SIZE, AM_FILE);
+    ATOMIC_MIXED_ACCESSOR_ATTRIBUTE("Text", GetTextAttr, SetTextAttr, String, String::EMPTY, AM_FILE);
+    ATOMIC_ENUM_ATTRIBUTE("Text Alignment", textAlignment_, horizontalAlignments, HA_LEFT, AM_FILE);
+    ATOMIC_ATTRIBUTE("Row Spacing", float, rowSpacing_, 1.0f, AM_FILE);
+    ATOMIC_ATTRIBUTE("Word Wrap", bool, wordWrap_, false, AM_FILE);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Auto Localizable", GetAutoLocalizable, SetAutoLocalizable, bool, false, AM_FILE);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Selection Color", GetSelectionColor, SetSelectionColor, Color, Color::TRANSPARENT, AM_FILE);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Hover Color", GetHoverColor, SetHoverColor, Color, Color::TRANSPARENT, AM_FILE);
+    ATOMIC_ENUM_ATTRIBUTE("Text Effect", textEffect_, textEffects, TE_NONE, AM_FILE);
+    ATOMIC_ATTRIBUTE("Shadow Offset", IntVector2, shadowOffset_, IntVector2(1, 1), AM_FILE);
+    ATOMIC_ATTRIBUTE("Stroke Thickness", int, strokeThickness_, 1, AM_FILE);
+    ATOMIC_ATTRIBUTE("Round Stroke", bool, roundStroke_, false, AM_FILE);
+    ATOMIC_ACCESSOR_ATTRIBUTE("Effect Color", GetEffectColor, SetEffectColor, Color, Color::BLACK, AM_FILE);
+}
+
+void Text3DText::ApplyAttributes()
+{
+    Animatable::ApplyAttributes();
+
+    colorGradient_ = false;
+
+    for (unsigned i = 1; i < MAX_TEXT_CORNERS; ++i)
+    {
+        if (color_[i] != color_[0])
+            colorGradient_ = true;
+    }
+
+    // Localize now if attributes were loaded out-of-order
+    if (autoLocalizable_ && stringId_.Length())
+    {
+        Localization* l10n = GetSubsystem<Localization>();
+        text_ = l10n->Get(stringId_);
+    }
+
+    DecodeToUnicode();
+
+    fontSize_ = Max(fontSize_, 1);
+    strokeThickness_ = Abs(strokeThickness_);
+    ValidateSelection();
+    UpdateText();
+}
+
+void Text3DText::OnAttributeAnimationAdded()
+{
+    if (attributeAnimationInfos_.Size() == 1)
+        SubscribeToEvent(E_POSTUPDATE, ATOMIC_HANDLER(Text3DText, HandlePostUpdate));
+}
+
+void Text3DText::OnAttributeAnimationRemoved()
+{
+    if (attributeAnimationInfos_.Empty())
+        UnsubscribeFromEvent(E_POSTUPDATE);
+}
+
+
+void Text3DText::SetColor(const Color& color)
+{
+    for (unsigned i = 0; i < MAX_TEXT_CORNERS; ++i)
+        color_[i] = color;
+
+    colorGradient_ = false;
+}
+
+void Text3DText::SetColor(Text3DCorner corner, const Color& color)
+{
+    color_[corner] = color;
+    colorGradient_ = false;
+
+    for (unsigned i = 0; i < MAX_TEXT_CORNERS; ++i)
+    {
+        if (i != corner && color_[i] != color_[corner])
+            colorGradient_ = true;
+    }
+}
+
+void Text3DText::SetOpacity(float opacity)
+{
+    opacity_ = Clamp(opacity, 0.0f, 1.0f);
+    MarkDirty();
+}
+
+void Text3DText::MarkDirty()
+{
+
+}
+
+void Text3DText::GetBatches(PODVector<Text3DBatch>& batches, PODVector<float>& vertexData, const IntRect& currentScissor)
+{    
+    Text3DFontFace* face = font_ ? font_->GetFace(fontSize_) : (Text3DFontFace*)0;
+    if (!face)
+    {
+        hovering_ = false;
+        return;
+    }
+
+    // If face has changed or char locations are not valid anymore, update before rendering
+    if (charLocationsDirty_ || !fontFace_ || face != fontFace_)
+        UpdateCharLocations();
+    // If face uses mutable glyphs mechanism, reacquire glyphs before rendering to make sure they are in the texture
+    else if (face->HasMutableGlyphs())
+    {
+        for (unsigned i = 0; i < printText_.Size(); ++i)
+            face->GetGlyph(printText_[i]);
+    }
+
+    // Hovering and/or whole selection batch
+    if ((hovering_ && hoverColor_.a_ > 0.0) || (selected_ && selectionColor_.a_ > 0.0f))
+    {
+        bool both = hovering_ && selected_ && hoverColor_.a_ > 0.0 && selectionColor_.a_ > 0.0f;
+        Text3DBatch batch(this, BLEND_ALPHA, currentScissor, 0, &vertexData);
+        batch.SetColor(both ? selectionColor_.Lerp(hoverColor_, 0.5f) :
+            (selected_ && selectionColor_.a_ > 0.0f ? selectionColor_ : hoverColor_));
+        batch.AddQuad(0, 0, GetWidth(), GetHeight(), 0, 0);
+        Text3DBatch::AddOrMerge(batch, batches);
+    }
+
+    // Partial selection batch
+    if (!selected_ && selectionLength_ && charLocations_.Size() >= selectionStart_ + selectionLength_ && selectionColor_.a_ > 0.0f)
+    {
+        Text3DBatch batch(this, BLEND_ALPHA, currentScissor, 0, &vertexData);
+        batch.SetColor(selectionColor_);
+
+        IntVector2 currentStart = charLocations_[selectionStart_].position_;
+        IntVector2 currentEnd = currentStart;
+        for (unsigned i = selectionStart_; i < selectionStart_ + selectionLength_; ++i)
+        {
+            // Check if row changes, and start a new quad in that case
+            if (charLocations_[i].size_ != IntVector2::ZERO)
+            {
+                if (charLocations_[i].position_.y_ != currentStart.y_)
+                {
+                    batch.AddQuad(currentStart.x_, currentStart.y_, currentEnd.x_ - currentStart.x_,
+                        currentEnd.y_ - currentStart.y_, 0, 0);
+                    currentStart = charLocations_[i].position_;
+                    currentEnd = currentStart + charLocations_[i].size_;
+                }
+                else
+                {
+                    currentEnd.x_ += charLocations_[i].size_.x_;
+                    currentEnd.y_ = Max(currentStart.y_ + charLocations_[i].size_.y_, currentEnd.y_);
+                }
+            }
+        }
+        if (currentEnd != currentStart)
+        {
+            batch.AddQuad(currentStart.x_, currentStart.y_, currentEnd.x_ - currentStart.x_, currentEnd.y_ - currentStart.y_, 0, 0);
+        }
+
+        Text3DBatch::AddOrMerge(batch, batches);
+    }
+
+    // Text batch
+    Text3DTextEffect textEffect = font_->IsSDFFont() ? TE_NONE : textEffect_;
+    const Vector<SharedPtr<Texture2D> >& textures = face->GetTextures();
+    for (unsigned n = 0; n < textures.Size() && n < pageGlyphLocations_.Size(); ++n)
+    {
+        // One batch per texture/page
+        Text3DBatch pageBatch(this, BLEND_ALPHA, currentScissor, textures[n], &vertexData);
+
+        const PODVector<Text3DGlyphLocation>& pageGlyphLocation = pageGlyphLocations_[n];
+
+        switch (textEffect)
+        {
+        case TE_NONE:
+            ConstructBatch(pageBatch, pageGlyphLocation, 0, 0);
+            break;
+
+        case TE_SHADOW:
+            ConstructBatch(pageBatch, pageGlyphLocation, shadowOffset_.x_, shadowOffset_.y_, &effectColor_, effectDepthBias_);
+            ConstructBatch(pageBatch, pageGlyphLocation, 0, 0);
+            break;
+
+        case TE_STROKE:
+            if (roundStroke_)
+            {
+                // Samples should be even or glyph may be redrawn in wrong x y pos making stroke corners rough
+                // Adding to thickness helps with thickness of 1 not having enought samples for this formula
+                // or certain fonts with reflex corners requiring more glyph samples for a smooth stroke when large
+                int thickness = Min(strokeThickness_, fontSize_);
+                int samples = thickness * thickness + (thickness % 2 == 0 ? 4 : 3);
+                float angle = 360.f / samples;
+                float floatThickness = (float)thickness;
+                for (int i = 0; i < samples; ++i)
+                {
+                    float x = Cos(angle * i) * floatThickness;
+                    float y = Sin(angle * i) * floatThickness;
+                    ConstructBatch(pageBatch, pageGlyphLocation, (int)x, (int)y, &effectColor_, effectDepthBias_);
+                }
+            }
+            else
+            {
+                int thickness = Min(strokeThickness_, fontSize_);
+                int x, y;
+                for (x = -thickness; x <= thickness; ++x)
+                {
+                    for (y = -thickness; y <= thickness; ++y)
+                    {
+                        // Don't draw glyphs that aren't on the edges
+                        if (x > -thickness && x < thickness &&
+                            y > -thickness && y < thickness)
+                            continue;
+    
+                        ConstructBatch(pageBatch, pageGlyphLocation, x, y, &effectColor_, effectDepthBias_);
+                    }
+                }
+            }
+            ConstructBatch(pageBatch, pageGlyphLocation, 0, 0);
+            break;
+        }
+
+        Text3DBatch::AddOrMerge(pageBatch, batches);
+    }
+
+    // Reset hovering for next frame
+    hovering_ = false;
+}
+
+void Text3DText::OnResize(const IntVector2& newSize, const IntVector2& delta)
+{
+    if (wordWrap_)
+        UpdateText(true);
+    else
+        charLocationsDirty_ = true;
+}
+
+void Text3DText::OnIndentSet()
+{
+    charLocationsDirty_ = true;
+}
+
+bool Text3DText::SetFont(const String& fontName, int size)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    return SetFont(cache->GetResource<Text3DFont>(fontName), size);
+}
+
+bool Text3DText::SetFont(Text3DFont* font, int size)
+{
+    if (!font)
+    {
+        ATOMIC_LOGERROR("Null font for Text");
+        return false;
+    }
+
+    if (font != font_ || size != fontSize_)
+    {
+        font_ = font;
+        fontSize_ = Max(size, 1);
+        UpdateText();
+    }
+
+    return true;
+}
+
+bool Text3DText::SetFontSize(int size)
+{
+    // Initial font must be set
+    if (!font_)
+        return false;
+    else
+        return SetFont(font_, size);
+}
+
+void Text3DText::DecodeToUnicode()
+{
+    unicodeText_.Clear();
+    for (unsigned i = 0; i < text_.Length();)
+        unicodeText_.Push(text_.NextUTF8Char(i));
+}
+
+void Text3DText::SetText(const String& text)
+{
+    if (autoLocalizable_)
+    {
+        stringId_ = text;
+        Localization* l10n = GetSubsystem<Localization>();
+        text_ = l10n->Get(stringId_);
+    }
+    else
+    {
+        text_ = text;
+    }
+
+    DecodeToUnicode();
+    ValidateSelection();
+    UpdateText();
+}
+
+void Text3DText::SetTextAlignment(Text3DHorizontalAlignment align)
+{
+    if (align != textAlignment_)
+    {
+        textAlignment_ = align;
+        charLocationsDirty_ = true;
+    }
+}
+
+void Text3DText::SetRowSpacing(float spacing)
+{
+    if (spacing != rowSpacing_)
+    {
+        rowSpacing_ = Max(spacing, MIN_ROW_SPACING);
+        UpdateText();
+    }
+}
+
+void Text3DText::SetWordwrap(bool enable)
+{
+    if (enable != wordWrap_)
+    {
+        wordWrap_ = enable;
+        UpdateText();
+    }
+}
+
+void Text3DText::SetAutoLocalizable(bool enable)
+{
+    if (enable != autoLocalizable_)
+    {
+        autoLocalizable_ = enable;
+        if (enable)
+        {
+            stringId_ = text_;
+            Localization* l10n = GetSubsystem<Localization>();
+            text_ = l10n->Get(stringId_);
+            SubscribeToEvent(E_CHANGELANGUAGE, ATOMIC_HANDLER(Text3DText, HandleChangeLanguage));
+        }
+        else
+        {
+            text_ = stringId_;
+            stringId_ = "";
+            UnsubscribeFromEvent(E_CHANGELANGUAGE);
+        }
+        DecodeToUnicode();
+        ValidateSelection();
+        UpdateText();
+    }
+}
+
+void Text3DText::HandleChangeLanguage(StringHash eventType, VariantMap& eventData)
+{
+    Localization* l10n = GetSubsystem<Localization>();
+    text_ = l10n->Get(stringId_);
+    DecodeToUnicode();
+    ValidateSelection();
+    UpdateText();
+}
+
+void Text3DText::SetSelection(unsigned start, unsigned length)
+{
+    selectionStart_ = start;
+    selectionLength_ = length;
+    ValidateSelection();
+}
+
+void Text3DText::ClearSelection()
+{
+    selectionStart_ = 0;
+    selectionLength_ = 0;
+}
+
+void Text3DText::SetSelectionColor(const Color& color)
+{
+    selectionColor_ = color;
+}
+
+void Text3DText::SetHoverColor(const Color& color)
+{
+    hoverColor_ = color;
+}
+
+void Text3DText::SetTextEffect(Text3DTextEffect textEffect)
+{
+    textEffect_ = textEffect;
+}
+
+void Text3DText::SetEffectShadowOffset(const IntVector2& offset)
+{
+    shadowOffset_ = offset;
+}
+
+void Text3DText::SetEffectStrokeThickness(int thickness)
+{
+    strokeThickness_ = Abs(thickness);
+}
+
+void Text3DText::SetEffectRoundStroke(bool roundStroke)
+{
+    roundStroke_ = roundStroke;
+}
+
+void Text3DText::SetEffectColor(const Color& effectColor)
+{
+    effectColor_ = effectColor;
+}
+
+void Text3DText::SetEffectDepthBias(float bias)
+{
+    effectDepthBias_ = bias;
+}
+
+int Text3DText::GetRowWidth(unsigned index) const
+{
+    return index < rowWidths_.Size() ? rowWidths_[index] : 0;
+}
+
+IntVector2 Text3DText::GetCharPosition(unsigned index)
+{
+    if (charLocationsDirty_)
+        UpdateCharLocations();
+    if (charLocations_.Empty())
+        return IntVector2::ZERO;
+    // For convenience, return the position of the text ending if index exceeded
+    if (index > charLocations_.Size() - 1)
+        index = charLocations_.Size() - 1;
+    return charLocations_[index].position_;
+}
+
+IntVector2 Text3DText::GetCharSize(unsigned index)
+{
+    if (charLocationsDirty_)
+        UpdateCharLocations();
+    if (charLocations_.Size() < 2)
+        return IntVector2::ZERO;
+    // For convenience, return the size of the last char if index exceeded (last size entry is zero)
+    if (index > charLocations_.Size() - 2)
+        index = charLocations_.Size() - 2;
+    return charLocations_[index].size_;
+}
+
+void Text3DText::SetFontAttr(const ResourceRef& value)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    font_ = cache->GetResource<Text3DFont>(value.name_);
+}
+
+ResourceRef Text3DText::GetFontAttr() const
+{
+    return GetResourceRef(font_, Text3DFont::GetTypeStatic());
+}
+
+void Text3DText::SetTextAttr(const String& value)
+{
+    text_ = value;
+    if (autoLocalizable_)
+        stringId_ = value;
+}
+
+String Text3DText::GetTextAttr() const
+{
+    if (autoLocalizable_ && stringId_.Length())
+        return stringId_;
+    else
+        return text_;
+}
+
+void Text3DText::UpdateText(bool onResize)
+{
+    rowWidths_.Clear();
+    printText_.Clear();
+
+    if (font_)
+    {
+        Text3DFontFace* face = font_->GetFace(fontSize_);
+        if (!face)
+            return;
+
+        rowHeight_ = face->GetRowHeight();
+
+        int width = 0;
+        int height = 0;
+        int rowWidth = 0;
+        int rowHeight = (int)(rowSpacing_ * rowHeight_);
+
+        // First see if the text must be split up
+        if (!wordWrap_)
+        {
+            printText_ = unicodeText_;
+            printToText_.Resize(printText_.Size());
+            for (unsigned i = 0; i < printText_.Size(); ++i)
+                printToText_[i] = i;
+        }
+        else
+        {
+            int maxWidth = GetWidth();
+            unsigned nextBreak = 0;
+            unsigned lineStart = 0;
+            printToText_.Clear();
+
+            for (unsigned i = 0; i < unicodeText_.Size(); ++i)
+            {
+                unsigned j;
+                unsigned c = unicodeText_[i];
+
+                if (c != '\n')
+                {
+                    bool ok = true;
+
+                    if (nextBreak <= i)
+                    {
+                        int futureRowWidth = rowWidth;
+                        for (j = i; j < unicodeText_.Size(); ++j)
+                        {
+                            unsigned d = unicodeText_[j];
+                            if (d == ' ' || d == '\n')
+                            {
+                                nextBreak = j;
+                                break;
+                            }
+                            const Text3DFontGlyph* glyph = face->GetGlyph(d);
+                            if (glyph)
+                            {
+                                futureRowWidth += glyph->advanceX_;
+                                if (j < unicodeText_.Size() - 1)
+                                    futureRowWidth += face->GetKerning(d, unicodeText_[j + 1]);
+                            }
+                            if (d == '-' && futureRowWidth <= maxWidth)
+                            {
+                                nextBreak = j + 1;
+                                break;
+                            }
+                            if (futureRowWidth > maxWidth)
+                            {
+                                ok = false;
+                                break;
+                            }
+                        }
+                    }
+
+                    if (!ok)
+                    {
+                        // If did not find any breaks on the line, copy until j, or at least 1 char, to prevent infinite loop
+                        if (nextBreak == lineStart)
+                        {
+                            while (i < j)
+                            {
+                                printText_.Push(unicodeText_[i]);
+                                printToText_.Push(i);
+                                ++i;
+                            }
+                        }
+                        // Eliminate spaces that have been copied before the forced break
+                        while (printText_.Size() && printText_.Back() == ' ')
+                        {
+                            printText_.Pop();
+                            printToText_.Pop();
+                        }
+                        printText_.Push('\n');
+                        printToText_.Push(Min(i, unicodeText_.Size() - 1));
+                        rowWidth = 0;
+                        nextBreak = lineStart = i;
+                    }
+
+                    if (i < unicodeText_.Size())
+                    {
+                        // When copying a space, position is allowed to be over row width
+                        c = unicodeText_[i];
+                        const Text3DFontGlyph* glyph = face->GetGlyph(c);
+                        if (glyph)
+                        {
+                            rowWidth += glyph->advanceX_;
+                            if (i < unicodeText_.Size() - 1)
+                                rowWidth += face->GetKerning(c, unicodeText_[i + 1]);
+                        }
+                        if (rowWidth <= maxWidth)
+                        {
+                            printText_.Push(c);
+                            printToText_.Push(i);
+                        }
+                    }
+                }
+                else
+                {
+                    printText_.Push('\n');
+                    printToText_.Push(Min(i, unicodeText_.Size() - 1));
+                    rowWidth = 0;
+                    nextBreak = lineStart = i;
+                }
+            }
+        }
+
+        rowWidth = 0;
+
+        for (unsigned i = 0; i < printText_.Size(); ++i)
+        {
+            unsigned c = printText_[i];
+
+            if (c != '\n')
+            {
+                const Text3DFontGlyph* glyph = face->GetGlyph(c);
+                if (glyph)
+                {
+                    rowWidth += glyph->advanceX_;
+                    if (i < printText_.Size() - 1)
+                        rowWidth += face->GetKerning(c, printText_[i + 1]);
+                }
+            }
+            else
+            {
+                width = Max(width, rowWidth);
+                height += rowHeight;
+                rowWidths_.Push(rowWidth);
+                rowWidth = 0;
+            }
+        }
+
+        if (rowWidth)
+        {
+            width = Max(width, rowWidth);
+            height += rowHeight;
+            rowWidths_.Push(rowWidth);
+        }
+
+        // Set at least one row height even if text is empty
+        if (!height)
+            height = rowHeight;
+
+        // Set minimum and current size according to the text size, but respect fixed width if set
+        if (!IsFixedWidth())
+        {
+            SetMinWidth(wordWrap_ ? 0 : width);
+            SetWidth(width);
+        }
+
+        SetFixedHeight(height);
+
+        charLocationsDirty_ = true;
+    }
+    else
+    {
+        // No font, nothing to render
+        pageGlyphLocations_.Clear();
+    }
+}
+
+void Text3DText::UpdateCharLocations()
+{
+    // Remember the font face to see if it's still valid when it's time to render
+    Text3DFontFace* face = font_ ? font_->GetFace(fontSize_) : (Text3DFontFace*)0;
+    if (!face)
+        return;
+    fontFace_ = face;
+
+    int rowHeight = (int)(rowSpacing_ * rowHeight_);
+
+    // Store position & size of each character, and locations per texture page
+    unsigned numChars = unicodeText_.Size();
+    charLocations_.Resize(numChars + 1);
+    pageGlyphLocations_.Resize(face->GetTextures().Size());
+    for (unsigned i = 0; i < pageGlyphLocations_.Size(); ++i)
+        pageGlyphLocations_[i].Clear();
+
+    IntVector2 offset = font_->GetTotalGlyphOffset(fontSize_);
+
+    unsigned rowIndex = 0;
+    unsigned lastFilled = 0;
+    int x = GetRowStartPosition(rowIndex) + offset.x_;
+    int y = offset.y_;
+
+    for (unsigned i = 0; i < printText_.Size(); ++i)
+    {
+        Text3DCharLocation loc;
+        loc.position_ = IntVector2(x, y);
+
+        unsigned c = printText_[i];
+        if (c != '\n')
+        {
+            const Text3DFontGlyph* glyph = face->GetGlyph(c);
+            loc.size_ = IntVector2(glyph ? glyph->advanceX_ : 0, rowHeight_);
+            if (glyph)
+            {
+                // Store glyph's location for rendering. Verify that glyph page is valid
+                if (glyph->page_ < pageGlyphLocations_.Size())
+                    pageGlyphLocations_[glyph->page_].Push(Text3DGlyphLocation(x, y, glyph));
+                x += glyph->advanceX_;
+                if (i < printText_.Size() - 1)
+                    x += face->GetKerning(c, printText_[i + 1]);
+            }
+        }
+        else
+        {
+            loc.size_ = IntVector2::ZERO;
+            x = GetRowStartPosition(++rowIndex);
+            y += rowHeight;
+        }
+
+        if (lastFilled > printToText_[i])
+            lastFilled = printToText_[i];
+
+        // Fill gaps in case characters were skipped from printing
+        for (unsigned j = lastFilled; j <= printToText_[i]; ++j)
+            charLocations_[j] = loc;
+        lastFilled = printToText_[i] + 1;
+    }
+    // Store the ending position
+    charLocations_[numChars].position_ = IntVector2(x, y);
+    charLocations_[numChars].size_ = IntVector2::ZERO;
+
+    charLocationsDirty_ = false;
+}
+
+void Text3DText::ValidateSelection()
+{
+    unsigned textLength = unicodeText_.Size();
+
+    if (textLength)
+    {
+        if (selectionStart_ >= textLength)
+            selectionStart_ = textLength - 1;
+        if (selectionStart_ + selectionLength_ > textLength)
+            selectionLength_ = textLength - selectionStart_;
+    }
+    else
+    {
+        selectionStart_ = 0;
+        selectionLength_ = 0;
+    }
+}
+
+int Text3DText::GetRowStartPosition(unsigned rowIndex) const
+{
+    int rowWidth = 0;
+
+    if (rowIndex < rowWidths_.Size())
+        rowWidth = rowWidths_[rowIndex];
+
+    int ret = GetIndentWidth();
+
+    switch (textAlignment_)
+    {
+    case HA_LEFT:
+        break;
+    case HA_CENTER:
+        ret += (GetSize().x_ - rowWidth) / 2;
+        break;
+    case HA_RIGHT:
+        ret += GetSize().x_ - rowWidth;
+        break;
+    }
+
+    return ret;
+}
+
+void Text3DText::SetIndent(int indent)
+{
+    indent_ = indent;
+    OnIndentSet();
+}
+
+void Text3DText::SetIndentSpacing(int indentSpacing)
+{
+    indentSpacing_ = Max(indentSpacing, 0);
+    OnIndentSet();
+}
+
+void Text3DText::ConstructBatch(Text3DBatch& pageBatch, const PODVector<Text3DGlyphLocation>& pageGlyphLocation, int dx, int dy, Color* color,
+    float depthBias)
+{
+    unsigned startDataSize = pageBatch.vertexData_->Size();
+
+    if (!color)
+        pageBatch.SetDefaultColor();
+    else
+        pageBatch.SetColor(*color);
+
+    for (unsigned i = 0; i < pageGlyphLocation.Size(); ++i)
+    {
+        const Text3DGlyphLocation& glyphLocation = pageGlyphLocation[i];
+        const Text3DFontGlyph& glyph = *glyphLocation.glyph_;
+        pageBatch.AddQuad(dx + glyphLocation.x_ + glyph.offsetX_, dy + glyphLocation.y_ + glyph.offsetY_, glyph.width_,
+            glyph.height_, glyph.x_, glyph.y_);
+    }
+
+    if (depthBias != 0.0f)
+    {
+        unsigned dataSize = pageBatch.vertexData_->Size();
+        for (unsigned i = startDataSize; i < dataSize; i += TEXT3D_VERTEX_SIZE)
+            pageBatch.vertexData_->At(i + 2) += depthBias;
+    }
+}
+
+void Text3DText::HandlePostUpdate(StringHash eventType, VariantMap& eventData)
+{
+    using namespace PostUpdate;
+
+    UpdateAttributeAnimations(eventData[P_TIMESTEP].GetFloat());
+}
+
+void Text3DText::SetSelected(bool enable)
+{
+    selected_ = enable;
+}
+
+void Text3DText::SetHovering(bool enable)
+{
+    hovering_ = enable;
+}
+
+void Text3DText::SetWidth(int width)
+{
+    SetSize(IntVector2(width, size_.y_));
+}
+
+void Text3DText::SetHeight(int height)
+{
+    SetSize(IntVector2(size_.x_, height));
+}
+
+void Text3DText::SetMinSize(const IntVector2& minSize)
+{
+    minSize_.x_ = Max(minSize.x_, 0);
+    minSize_.y_ = Max(minSize.y_, 0);
+    SetSize(size_);
+}
+
+void Text3DText::SetMinSize(int width, int height)
+{
+    SetMinSize(IntVector2(width, height));
+}
+
+void Text3DText::SetMinWidth(int width)
+{
+    SetMinSize(IntVector2(width, minSize_.y_));
+}
+
+void Text3DText::SetMinHeight(int height)
+{
+    SetMinSize(IntVector2(minSize_.x_, height));
+}
+
+void Text3DText::SetMaxSize(const IntVector2& maxSize)
+{
+    maxSize_.x_ = Max(maxSize.x_, 0);
+    maxSize_.y_ = Max(maxSize.y_, 0);
+    SetSize(size_);
+}
+
+void Text3DText::SetMaxSize(int width, int height)
+{
+    SetMaxSize(IntVector2(width, height));
+}
+
+void Text3DText::SetMaxWidth(int width)
+{
+    SetMaxSize(IntVector2(width, maxSize_.y_));
+}
+
+void Text3DText::SetMaxHeight(int height)
+{
+    SetMaxSize(IntVector2(maxSize_.x_, height));
+}
+
+void Text3DText::SetFixedSize(const IntVector2& size)
+{
+    minSize_ = maxSize_ = IntVector2(Max(size.x_, 0), Max(size.y_, 0));
+    SetSize(size);
+}
+
+void Text3DText::SetFixedSize(int width, int height)
+{
+    SetFixedSize(IntVector2(width, height));
+}
+
+void Text3DText::SetFixedWidth(int width)
+{
+    minSize_.x_ = maxSize_.x_ = Max(width, 0);
+    SetWidth(width);
+}
+
+void Text3DText::SetFixedHeight(int height)
+{
+    minSize_.y_ = maxSize_.y_ = Max(height, 0);
+    SetHeight(height);
+}
+
+void Text3DText::SetSize(const IntVector2& size)
+{
+    IntVector2 oldSize = size_;
+    size_ = size;
+
+    IntVector2 delta = size_ - oldSize;
+    MarkDirty();
+    OnResize(size_, delta);
+}
+
+
+}

+ 468 - 0
Source/Atomic/Graphics/Text3D/Text3DText.h

@@ -0,0 +1,468 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../../Scene/Animatable.h"
+#include "Text3DBatch.h"
+
+namespace Atomic
+{
+
+static const int TEXT3D_DEFAULT_FONT_SIZE = 12;
+
+class Text3DFont;
+class Text3DFontFace;
+struct Text3DFontGlyph;
+
+/// Text effect.
+enum Text3DTextEffect
+{
+    TE_NONE = 0,
+    TE_SHADOW,
+    TE_STROKE
+};
+
+/// Cached character location and size within text. Used for queries related to text editing.
+struct Text3DCharLocation
+{
+    /// Position.
+    IntVector2 position_;
+    /// Size.
+    IntVector2 size_;
+};
+
+/// Glyph and its location within the text. Used when preparing text rendering.
+struct Text3DGlyphLocation
+{
+    /// Construct.
+    Text3DGlyphLocation(int x, int y, const Text3DFontGlyph* glyph) :
+        x_(x),
+        y_(y),
+        glyph_(glyph)
+    {
+    }
+
+    /// X coordinate.
+    int x_;
+    /// Y coordinate.
+    int y_;
+    /// Glyph.
+    const Text3DFontGlyph* glyph_;
+};
+
+
+enum Text3DHorizontalAlignment
+{
+    HA_LEFT = 0,
+    HA_CENTER,
+    HA_RIGHT,
+    HA_CUSTOM
+};
+
+
+enum Text3DVerticalAlignment
+{
+    VA_TOP = 0,
+    VA_CENTER,
+    VA_BOTTOM,
+    VA_CUSTOM
+};
+
+enum Text3DCorner
+{
+    C_TOPLEFT = 0,
+    C_TOPRIGHT,
+    C_BOTTOMLEFT,
+    C_BOTTOMRIGHT,
+    MAX_TEXT_CORNERS
+};
+
+class ATOMIC_API Text3DText : public Animatable
+{
+    ATOMIC_OBJECT(Text3DText, Animatable)
+
+    friend class Text3D;
+
+public:
+    /// Construct.
+    Text3DText(Context* context);
+    /// Destruct.
+    virtual ~Text3DText();
+    /// Register object factory.
+    static void RegisterObject(Context* context);
+
+    /// Apply attribute changes that can not be applied immediately.
+    virtual void ApplyAttributes();
+    /// Return UI rendering batches.
+    virtual void GetBatches(PODVector<Text3DBatch>& batches, PODVector<float>& vertexData, const IntRect& currentScissor);
+    /// React to resize.
+    virtual void OnResize(const IntVector2& newSize, const IntVector2& delta);
+    /// React to indent change.
+    virtual void OnIndentSet();
+
+    /// Set font by looking from resource cache by name and font size. Return true if successful.
+    bool SetFont(const String& fontName, int size = TEXT3D_DEFAULT_FONT_SIZE);
+    /// Set font and font size. Return true if successful.
+    bool SetFont(Text3DFont* font, int size = TEXT3D_DEFAULT_FONT_SIZE);
+    /// Set font size only while retaining the existing font. Return true if successful.
+    bool SetFontSize(int size);
+    /// Set text. Text is assumed to be either ASCII or UTF8-encoded.
+    void SetText(const String& text);
+    /// Set row alignment.
+    void SetTextAlignment(Text3DHorizontalAlignment align);
+    /// Set row spacing, 1.0 for original font spacing.
+    void SetRowSpacing(float spacing);
+    /// Set wordwrap. In wordwrap mode the text element will respect its current width. Otherwise it resizes itself freely.
+    void SetWordwrap(bool enable);
+    /// The text will be automatically translated. The text value used as string identifier.
+    void SetAutoLocalizable(bool enable);
+    /// Set selection. When length is not provided, select until the text ends.
+    void SetSelection(unsigned start, unsigned length = M_MAX_UNSIGNED);
+    /// Clear selection.
+    void ClearSelection();
+    /// Set selection background color. Color with 0 alpha (default) disables.
+    void SetSelectionColor(const Color& color);
+    /// Set hover background color. Color with 0 alpha (default) disables.
+    void SetHoverColor(const Color& color);
+    /// Set text effect.
+    void SetTextEffect(Text3DTextEffect textEffect);
+    /// Set shadow offset.
+    void SetEffectShadowOffset(const IntVector2& offset);
+    /// Set stroke thickness.
+    void SetEffectStrokeThickness(int thickness);
+    /// Set stroke rounding. Corners of the font will be rounded off in the stroke so the stroke won't have corners.
+    void SetEffectRoundStroke(bool roundStroke);
+    /// Set effect color.
+    void SetEffectColor(const Color& effectColor);
+
+    /// Return font.
+    Text3DFont* GetFont() const { return font_; }
+
+    /// Return font size.
+    int GetFontSize() const { return fontSize_; }
+
+    /// Return text.
+    const String& GetText() const { return text_; }
+
+    /// Return row alignment.
+    Text3DHorizontalAlignment GetTextAlignment() const { return textAlignment_; }
+
+    /// Return row spacing.
+    float GetRowSpacing() const { return rowSpacing_; }
+
+    /// Return wordwrap mode.
+    bool GetWordwrap() const { return wordWrap_; }
+
+    /// Return auto localizable mode.
+    bool GetAutoLocalizable() const { return autoLocalizable_; }
+
+    /// Return selection start.
+    unsigned GetSelectionStart() const { return selectionStart_; }
+
+    /// Return selection length.
+    unsigned GetSelectionLength() const { return selectionLength_; }
+
+    /// Return selection background color.
+    const Color& GetSelectionColor() const { return selectionColor_; }
+
+    /// Return hover background color.
+    const Color& GetHoverColor() const { return hoverColor_; }
+
+    /// Return text effect.
+    Text3DTextEffect GetTextEffect() const { return textEffect_; }
+
+    /// Return effect shadow offset.
+    const IntVector2& GetEffectShadowOffset() const { return shadowOffset_; }
+
+    /// Return effect stroke thickness.
+    int GetEffectStrokeThickness() const { return strokeThickness_; }
+
+    /// Return effect round stroke.
+    bool GetEffectRoundStroke() const { return roundStroke_; }
+
+    /// Return effect color.
+    const Color& GetEffectColor() const { return effectColor_; }
+
+    /// Return row height.
+    int GetRowHeight() const { return rowHeight_; }
+
+    /// Return number of rows.
+    unsigned GetNumRows() const { return rowWidths_.Size(); }
+
+    /// Return number of characters.
+    unsigned GetNumChars() const { return unicodeText_.Size(); }
+
+    /// Return width of row by index.
+    int GetRowWidth(unsigned index) const;
+    /// Return position of character by index relative to the text element origin.
+    IntVector2 GetCharPosition(unsigned index);
+    /// Return size of character by index.
+    IntVector2 GetCharSize(unsigned index);
+
+    /// Set text effect Z bias. Zero by default, adjusted only in 3D mode.
+    void SetEffectDepthBias(float bias);
+
+    /// Return effect Z bias.
+    float GetEffectDepthBias() const { return effectDepthBias_; }
+
+    /// Set size.
+    void SetSize(const IntVector2& size);
+    /// Set size.
+    void SetSize(int width, int height);
+    /// Set width only.
+    void SetWidth(int width);
+    /// Set height only.
+    void SetHeight(int height);
+    /// Set minimum size.
+    void SetMinSize(const IntVector2& minSize);
+    /// Set minimum size.
+    void SetMinSize(int width, int height);
+    /// Set minimum width.
+    void SetMinWidth(int width);
+    /// Set minimum height.
+    void SetMinHeight(int height);
+    /// Set maximum size.
+    void SetMaxSize(const IntVector2& maxSize);
+    /// Set maximum size.
+    void SetMaxSize(int width, int height);
+    /// Set maximum width.
+    void SetMaxWidth(int width);
+    /// Set maximum height.
+    void SetMaxHeight(int height);
+    /// Set fixed size.
+    void SetFixedSize(const IntVector2& size);
+    /// Set fixed size.
+    void SetFixedSize(int width, int height);
+    /// Set fixed width.
+    void SetFixedWidth(int width);
+    /// Set fixed height.
+    void SetFixedHeight(int height);
+
+    /// Return size.
+    const IntVector2& GetSize() const { return size_; }
+
+    /// Return width.
+    int GetWidth() const { return size_.x_; }
+
+    /// Return height.
+    int GetHeight() const { return size_.y_; }
+
+    /// Return minimum size.
+    const IntVector2& GetMinSize() const { return minSize_; }
+
+    /// Return minimum width.
+    int GetMinWidth() const { return minSize_.x_; }
+
+    /// Return minimum height.
+    int GetMinHeight() const { return minSize_.y_; }
+
+    /// Return maximum size.
+    const IntVector2& GetMaxSize() const { return maxSize_; }
+
+    /// Return minimum width.
+    int GetMaxWidth() const { return maxSize_.x_; }
+
+    /// Return minimum height.
+    int GetMaxHeight() const { return maxSize_.y_; }
+
+    /// Return true if size is fixed.
+    bool IsFixedSize() const { return minSize_ == maxSize_; }
+
+    /// Return true if width is fixed.
+    bool IsFixedWidth() const { return minSize_.x_ == maxSize_.x_; }
+
+    /// Return true if height is fixed.
+    bool IsFixedHeight() const { return minSize_.y_ == maxSize_.y_; }
+
+    /// Set horizontal indentation.
+    void SetIndent(int indent);
+    /// Set indent spacing (number of pixels per indentation level).
+    void SetIndentSpacing(int indentSpacing);
+
+    /// Return horizontal indentation.
+    int GetIndent() const { return indent_; }
+
+    /// Return indent spacing (number of pixels per indentation level).
+    int GetIndentSpacing() const { return indentSpacing_; }
+
+    /// Return indent width in pixels.
+    int GetIndentWidth() const { return indent_ * indentSpacing_; }
+
+
+    /// Return corner color.
+    const Color& GetColor(Text3DCorner corner) const { return color_[corner]; }
+
+    const Color& GetColor() const { return color_[C_TOPLEFT]; }
+
+    /// Set color on all corners.
+    void SetColor(const Color& color);
+
+    /// Set color on one corner.
+    void SetColor(Text3DCorner corner, const Color& color);
+
+    /// Return whether has different color in at least one corner.
+    bool HasColorGradient() const { return colorGradient_; }
+
+    /// Set opacity.
+    void SetOpacity(float opacity);
+
+    /// Return opacity.
+    float GetOpacity() const { return opacity_; }
+
+
+    /// Return whether is selected.
+    bool IsSelected() const { return selected_; }
+
+    /// Set selected mode.
+    void SetSelected(bool enable);
+
+    /// Return whether the cursor is hovering on this element.
+    bool IsHovering() const { return hovering_; }
+
+    /// Set hovering state.
+    void SetHovering(bool enable);
+
+    /// Set font attribute.
+    void SetFontAttr(const ResourceRef& value);
+    /// Return font attribute.
+    ResourceRef GetFontAttr() const;
+    /// Set text attribute.
+    void SetTextAttr(const String& value);
+    /// Return text attribute.
+    String GetTextAttr() const;
+
+protected:
+
+    /// Handle attribute animation added.
+    virtual void OnAttributeAnimationAdded();
+    /// Handle attribute animation removed.
+    virtual void OnAttributeAnimationRemoved();
+
+    virtual void MarkDirty();
+
+    /// Update text when text, font or spacing changed.
+    void UpdateText(bool onResize = false);
+    /// Update cached character locations after text update, or when text alignment or indent has changed.
+    void UpdateCharLocations();
+    /// Validate text selection to be within the text.
+    void ValidateSelection();
+    /// Return row start X position.
+    int GetRowStartPosition(unsigned rowIndex) const;
+    /// Contruct batch.
+    void ConstructBatch
+        (Text3DBatch& pageBatch, const PODVector<Text3DGlyphLocation>& pageGlyphLocation, int dx = 0, int dy = 0, Color* color = 0,
+            float depthBias = 0.0f);
+
+    /// Font.
+    SharedPtr<Text3DFont> font_;
+    /// Current face.
+    WeakPtr<Text3DFontFace> fontFace_;
+    /// Font size.
+    int fontSize_;
+    /// UTF-8 encoded text.
+    String text_;
+    /// Row alignment.
+    Text3DHorizontalAlignment textAlignment_;
+    /// Row spacing.
+    float rowSpacing_;
+    /// Wordwrap mode.
+    bool wordWrap_;
+    /// Char positions dirty flag.
+    bool charLocationsDirty_;
+    /// Selection start.
+    unsigned selectionStart_;
+    /// Selection length.
+    unsigned selectionLength_;
+    /// Selection background color.
+    Color selectionColor_;
+    /// Hover background color.
+    Color hoverColor_;
+    /// Text effect.
+    Text3DTextEffect textEffect_;
+    /// Text effect shadow offset.
+    IntVector2 shadowOffset_;
+    /// Text effect stroke thickness.
+    int strokeThickness_;
+    /// Text effect stroke rounding flag.
+    bool roundStroke_;
+    /// Effect color.
+    Color effectColor_;
+    /// Text effect Z bias.
+    float effectDepthBias_;
+    /// Row height.
+    int rowHeight_;
+    /// Text as Unicode characters.
+    PODVector<unsigned> unicodeText_;
+    /// Text modified into printed form.
+    PODVector<unsigned> printText_;
+    /// Mapping of printed form back to original char indices.
+    PODVector<unsigned> printToText_;
+    /// Row widths.
+    PODVector<int> rowWidths_;
+    /// Glyph locations per each texture in the font.
+    Vector<PODVector<Text3DGlyphLocation> > pageGlyphLocations_;
+    /// Cached locations of each character in the text.
+    PODVector<Text3DCharLocation> charLocations_;
+    /// The text will be automatically translated.
+    bool autoLocalizable_;
+    /// Localization string id storage. Used when autoLocalizable flag is set.
+    String stringId_;
+    /// Handle change Language.
+    void HandleChangeLanguage(StringHash eventType, VariantMap& eventData);
+    /// UTF8 to Unicode.
+    void DecodeToUnicode();
+
+    /// Size.
+    IntVector2 size_;
+    /// Minimum size.
+    IntVector2 minSize_;
+    /// Maximum size.
+    IntVector2 maxSize_;
+
+    /// Horizontal indentation.
+    int indent_;
+    /// Indent spacing (number of pixels per indentation level).
+    int indentSpacing_;
+
+    /// Colors.
+    Color color_[MAX_TEXT_CORNERS];
+    /// Has color gradient flag.
+    bool colorGradient_;
+
+    /// Opacity.
+    float opacity_;
+
+    /// Selected flag.
+    bool selected_;
+
+    /// Hovering flag.
+    bool hovering_;
+
+private:
+
+    /// Handle logic post-update event.
+    void HandlePostUpdate(StringHash eventType, VariantMap& eventData);
+
+};
+
+}