// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. #include "TextRender.h" #include "Engine/Core/Math/OrientedBoundingBox.h" #include "Engine/Engine/Engine.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/Models/Types.h" #include "Engine/Graphics/RenderView.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Level/Scene/SceneRendering.h" #include "Engine/Render2D/Font.h" #include "Engine/Render2D/FontAsset.h" #include "Engine/Render2D/FontManager.h" #include "Engine/Render2D/FontTextureAtlas.h" #include "Engine/Renderer/RenderList.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Content/Assets/MaterialInstance.h" #include "Engine/Content/Content.h" #include "Engine/Core/Types/Variant.h" #include "Engine/Localization/Localization.h" #if USE_EDITOR #include "Editor/Editor.h" #endif TextRender::TextRender(const SpawnParams& params) : Actor(params) , _size(32) , _ib(0, sizeof(uint16)) , _vb0(0, sizeof(VB0ElementType)) , _vb1(0, sizeof(VB1ElementType)) , _vb2(0, sizeof(VB2ElementType)) { _color = Color::White; _localBox = BoundingBox(Vector3::Zero); _layoutOptions.Bounds = Rectangle(-100, -100, 200, 200); _layoutOptions.HorizontalAlignment = TextAlignment::Center; _layoutOptions.VerticalAlignment = TextAlignment::Center; _layoutOptions.TextWrapping = TextWrapping::NoWrap; _layoutOptions.Scale = 1.0f; _layoutOptions.BaseLinesGapScale = 1.0f; // Link events Font.Changed.Bind(this); Font.Unload.Bind(this); Material.Changed.Bind(this); } const LocalizedString& TextRender::GetText() const { return _text; } void TextRender::SetText(const LocalizedString& value) { if (_text != value) { _text = value; _isDirty = true; } } Color TextRender::GetColor() const { return _color; } void TextRender::SetColor(const Color& value) { if (_color != value) { _color = value; _isDirty = true; } } int32 TextRender::GetFontSize() const { return _size; } void TextRender::SetFontSize(int32 value) { value = Math::Clamp(value, 1, 1024); if (_size != value) { _size = value; _isDirty = true; } } void TextRender::SetLayoutOptions(TextLayoutOptions& value) { if (_layoutOptions != value) { _layoutOptions = value; _isDirty = true; } } void TextRender::UpdateLayout() { // Clear _ib.Clear(); _vb0.Clear(); _vb1.Clear(); _vb2.Clear(); _localBox = BoundingBox(Vector3::Zero); BoundingBox::Transform(_localBox, _transform, _box); BoundingSphere::FromBox(_box, _sphere); #if USE_PRECISE_MESH_INTERSECTS _collisionProxy.Clear(); #endif // Skip if no font in use if (Font == nullptr || !Font->IsLoaded()) return; // Skip if material is not ready if (Material == nullptr || !Material->IsLoaded() || !Material->IsReady()) return; // Clear flag _isDirty = false; // Skip if no need to calculate the layout String textData; String* textPtr = &_text.Value; if (textPtr->IsEmpty()) { if (_text.Id.IsEmpty()) return; textData = Localization::GetString(_text.Id); textPtr = &textData; if (!_isLocalized) { _isLocalized = true; Localization::LocalizationChanged.Bind(this); } if (textPtr->IsEmpty()) return; } else if (_isLocalized) { _isLocalized = false; Localization::LocalizationChanged.Unbind(this); } const String& text = *textPtr; // Pick a font (remove DPI text scale as the text is being placed in the world) auto font = Font->CreateFont(_size); float scale = 1.0f / FontManager::FontScale; // Prepare FontTextureAtlas* fontAtlas = nullptr; Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; // Perform layout Array lines; font->ProcessText(text, lines, _layoutOptions); // Prepare buffers capacity _ib.Data.EnsureCapacity(text.Length() * 6 * sizeof(uint16)); _vb0.Data.EnsureCapacity(text.Length() * 4 * sizeof(VB0ElementType)); _vb1.Data.EnsureCapacity(text.Length() * 4 * sizeof(VB1ElementType)); _vb2.Data.EnsureCapacity(text.Length() * 4 * sizeof(VB2ElementType)); _buffersDirty = true; // Init draw chunks data DrawChunk drawChunk; drawChunk.Actor = this; drawChunk.StartIndex = 0; _drawChunks.Clear(); // Render all lines uint16 vertexCounter = 0; uint16 indexCounter = 0; FontCharacterEntry entry; BoundingBox box = BoundingBox::Empty; Color32 color(_color); for (int32 lineIndex = 0; lineIndex < lines.Count(); lineIndex++) { const FontLineCache& line = lines[lineIndex]; Float2 pointer = line.Location; // Render all characters from the line for (int32 charIndex = line.FirstCharIndex; charIndex <= line.LastCharIndex; charIndex++) { const Char c = text[charIndex]; if (c != '\n') { font->GetCharacter(c, entry); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != drawChunk.FontAtlasIndex) { // Check if need to change atlas (enqueue draw chunk) if (fontAtlas) { drawChunk.IndicesCount = (_ib.Data.Count() / sizeof(uint16)) - drawChunk.StartIndex; if (drawChunk.IndicesCount > 0) { _drawChunks.Add(drawChunk); } drawChunk.StartIndex = indexCounter; } // Get texture atlas that contains current character drawChunk.FontAtlasIndex = entry.TextureIndex; fontAtlas = FontManager::GetAtlas(drawChunk.FontAtlasIndex); if (fontAtlas) { fontAtlas->EnsureTextureCreated(); invAtlasSize = 1.0f / fontAtlas->GetSize(); } else { invAtlasSize = 1.0f; } // Setup material drawChunk.Material = Content::CreateVirtualAsset(); drawChunk.Material->SetBaseMaterial(Material.Get()); for (auto& param : drawChunk.Material->Params) param.SetIsOverride(false); // Set the font parameter static StringView FontParamName = TEXT("Font"); const auto param = drawChunk.Material->Params.Get(FontParamName); if (param && param->GetParameterType() == MaterialParameterType::Texture) { param->SetValue(Variant(fontAtlas)); param->SetIsOverride(true); } } // Get kerning const bool isWhitespace = StringUtils::IsWhitespace(c); if (!isWhitespace && previous.IsValid) { kerning = font->GetKerning(previous.Character, entry.Character); } else { kerning = 0; } pointer.X += (float)kerning * scale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates const float x = pointer.X + (float)entry.OffsetX * scale; const float y = pointer.Y + (float)(font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale; Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); charRect.Offset(_layoutOptions.Bounds.Location); Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; // Calculate bitangent sign Float3 normal = Float3::UnitZ; Float3 tangent = Float3::UnitX; byte sign = 0; // Write vertices VB0ElementType vb0; VB1ElementType vb1; VB2ElementType vb2; #define WRITE_VB(pos, uv) \ vb0.Position = Float3(-pos, 0.0f); \ box.Merge(vb0.Position); \ _vb0.Write(vb0); \ vb1.TexCoord = Half2(uv); \ vb1.Normal = Float1010102(normal * 0.5f + 0.5f, 0); \ vb1.Tangent = Float1010102(tangent * 0.5f + 0.5f, sign); \ vb1.LightmapUVs = Half2::Zero; \ _vb1.Write(vb1); \ vb2.Color = color; \ _vb2.Write(vb2) // WRITE_VB(charRect.GetBottomRight(), rightBottomUV); WRITE_VB(charRect.GetBottomLeft(), Float2(upperLeftUV.X, rightBottomUV.Y)); WRITE_VB(charRect.GetUpperLeft(), upperLeftUV); WRITE_VB(charRect.GetUpperRight(), Float2(rightBottomUV.X, upperLeftUV.Y)); // #undef WRITE_VB const uint16 startVertex = vertexCounter; vertexCounter += 4; indexCounter += 6; // Write indices _ib.Write((uint16)(startVertex + (uint16)0)); _ib.Write((uint16)(startVertex + (uint16)1)); _ib.Write((uint16)(startVertex + (uint16)2)); _ib.Write((uint16)(startVertex + (uint16)2)); _ib.Write((uint16)(startVertex + (uint16)3)); _ib.Write((uint16)(startVertex + (uint16)0)); } // Move pointer.X += (float)entry.AdvanceX * scale; } } } drawChunk.IndicesCount = (_ib.Data.Count() / sizeof(uint16)) - drawChunk.StartIndex; if (drawChunk.IndicesCount > 0) { _drawChunks.Add(drawChunk); } #if USE_PRECISE_MESH_INTERSECTS // Setup collision proxy for detailed collision detection for triangles const int32 totalIndicesCount = _ib.Data.Count() / sizeof(uint16); _collisionProxy.Init(_vb0.Data.Count() / sizeof(Float3), totalIndicesCount / 3, (Float3*)_vb0.Data.Get(), (uint16*)_ib.Data.Get()); #endif // Update text bounds (from build vertex positions) if (_ib.Data.IsEmpty()) { // Empty box = BoundingBox(_transform.Translation); } _localBox = box; BoundingBox::Transform(_localBox, _transform, _box); BoundingSphere::FromBox(_box, _sphere); if (_sceneRenderingKey != -1) GetSceneRendering()->UpdateActor(this, _sceneRenderingKey); } bool TextRender::HasContentLoaded() const { return (Material == nullptr || Material->IsLoaded()) && (Font == nullptr || Font->IsLoaded()); } void TextRender::Draw(RenderContext& renderContext) { if (renderContext.View.Pass == DrawPass::GlobalSDF) return; // TODO: Text rendering to Global SDF if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas) return; // TODO: Text rendering to Global Surface Atlas if (_isDirty) UpdateLayout(); Matrix world; renderContext.View.GetWorldMatrix(_transform, world); GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, world); const DrawPass drawModes = (DrawPass)(DrawModes & renderContext.View.Pass & (uint32)renderContext.View.GetShadowsDrawPassMask(ShadowsMode)); if (_vb0.Data.Count() > 0 && drawModes != DrawPass::None) { // Flush buffers if (_buffersDirty) { _buffersDirty = false; _ib.Flush(); _vb0.Flush(); _vb1.Flush(); _vb2.Flush(); } // Setup draw call DrawCall drawCall; drawCall.World = world; drawCall.ObjectPosition = drawCall.World.GetTranslation(); drawCall.Surface.GeometrySize = _localBox.GetSize(); drawCall.Surface.PrevWorld = _drawState.PrevWorld; drawCall.Surface.Lightmap = nullptr; drawCall.Surface.LightmapUVsArea = Rectangle::Empty; drawCall.Surface.Skinning = nullptr; drawCall.Surface.LODDitherFactor = 0.0f; drawCall.WorldDeterminantSign = Math::FloatSelect(world.RotDeterminant(), 1, -1); drawCall.PerInstanceRandom = GetPerInstanceRandom(); drawCall.Geometry.IndexBuffer = _ib.GetBuffer(); drawCall.Geometry.VertexBuffers[0] = _vb0.GetBuffer(); drawCall.Geometry.VertexBuffers[1] = _vb1.GetBuffer(); drawCall.Geometry.VertexBuffers[2] = _vb2.GetBuffer(); drawCall.Geometry.VertexBuffersOffsets[0] = 0; drawCall.Geometry.VertexBuffersOffsets[1] = 0; drawCall.Geometry.VertexBuffersOffsets[2] = 0; drawCall.InstanceCount = 1; // Submit draw calls for (const auto& e : _drawChunks) { if ((drawModes & e.Material->GetDrawModes()) == 0) continue; drawCall.Draw.IndicesCount = e.IndicesCount; drawCall.Draw.StartIndex = e.StartIndex; drawCall.Material = e.Material; renderContext.List->AddDrawCall(renderContext, drawModes, GetStaticFlags(), drawCall, true); } } GEOMETRY_DRAW_STATE_EVENT_END(_drawState, world); } #if USE_EDITOR #include "Engine/Debug/DebugDraw.h" void TextRender::OnDebugDrawSelected() { // Draw text bounds and layout bounds DEBUG_DRAW_WIRE_BOX(_box, Color::Orange, 0, true); OrientedBoundingBox layoutBox(Vector3(-_layoutOptions.Bounds.GetUpperLeft(), 0), Vector3(-_layoutOptions.Bounds.GetBottomRight(), 0)); layoutBox.Transform(_transform); DEBUG_DRAW_WIRE_BOX(layoutBox, Color::BlueViolet, 0, true); // Base Actor::OnDebugDrawSelected(); } #endif void TextRender::OnLayerChanged() { if (_sceneRenderingKey != -1) GetSceneRendering()->UpdateActor(this, _sceneRenderingKey); } bool TextRender::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) { #if USE_PRECISE_MESH_INTERSECTS if (_box.Intersects(ray)) { return _collisionProxy.Intersects(ray, _transform, distance, normal); } return false; #else return _box.Intersects(ray, distance, normal); #endif } void TextRender::Serialize(SerializeStream& stream, const void* otherObj) { // Base Actor::Serialize(stream, otherObj); SERIALIZE_GET_OTHER_OBJ(TextRender); SERIALIZE_MEMBER(Text, _text); SERIALIZE_MEMBER(Color, _color); SERIALIZE_MEMBER(Size, _size); SERIALIZE(Material); SERIALIZE(Font); SERIALIZE(ShadowsMode); SERIALIZE(DrawModes); SERIALIZE_MEMBER(Bounds, _layoutOptions.Bounds); SERIALIZE_MEMBER(HAlignment, _layoutOptions.HorizontalAlignment); SERIALIZE_MEMBER(VAlignment, _layoutOptions.VerticalAlignment); SERIALIZE_MEMBER(Wrapping, _layoutOptions.TextWrapping); SERIALIZE_MEMBER(Scale, _layoutOptions.Scale); SERIALIZE_MEMBER(GapScale, _layoutOptions.BaseLinesGapScale); } void TextRender::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { // Base Actor::Deserialize(stream, modifier); DESERIALIZE_MEMBER(Text, _text); DESERIALIZE_MEMBER(Color, _color); DESERIALIZE_MEMBER(Size, _size); DESERIALIZE(Material); DESERIALIZE(Font); DESERIALIZE(ShadowsMode); DESERIALIZE(DrawModes); DESERIALIZE_MEMBER(Bounds, _layoutOptions.Bounds); DESERIALIZE_MEMBER(HAlignment, _layoutOptions.HorizontalAlignment); DESERIALIZE_MEMBER(VAlignment, _layoutOptions.VerticalAlignment); DESERIALIZE_MEMBER(Wrapping, _layoutOptions.TextWrapping); DESERIALIZE_MEMBER(Scale, _layoutOptions.Scale); DESERIALIZE_MEMBER(GapScale, _layoutOptions.BaseLinesGapScale); // [Deprecated on 07.02.2022, expires on 07.02.2024] if (modifier->EngineBuild <= 6330) DrawModes |= DrawPass::GlobalSDF; // [Deprecated on 27.04.2022, expires on 27.04.2024] if (modifier->EngineBuild <= 6331) DrawModes |= DrawPass::GlobalSurfaceAtlas; _isDirty = true; } void TextRender::OnEnable() { // Base Actor::OnEnable(); if (_isDirty) { UpdateLayout(); } GetSceneRendering()->AddActor(this, _sceneRenderingKey); } void TextRender::OnDisable() { if (_isLocalized) { _isLocalized = false; Localization::LocalizationChanged.Unbind(this); } GetSceneRendering()->RemoveActor(this, _sceneRenderingKey); // Base Actor::OnDisable(); } void TextRender::OnTransformChanged() { // Base Actor::OnTransformChanged(); BoundingBox::Transform(_localBox, _transform, _box); BoundingSphere::FromBox(_box, _sphere); if (_sceneRenderingKey != -1) GetSceneRendering()->UpdateActor(this, _sceneRenderingKey); }