3D UI cleanup and update

Turns out 3D UI implementation was not flexible enough. This change allows ultimate freedom by splitting up responsibilities correctly.

UIComponent is no longer responsible for storing batches and vertex data. This information was moved to new `RenderToTextureData` struct inside `UI` subsystem. `UI` and `UIComponent` are no longer friends of each other.

New class `UIElement3D` was created. It is used as root UI element of UI rendered in a 3D scene. It implements `ScreenToElement()` method which is used for translating screen coordinates to element coordinates. Previously `UIComponent` did translation of these coordinates. `UIElement3D::ElementToScreen()` is not implemented at this time and returns `{-1, -1}` (invalid coordinates). It is also possible to set a custom viewport to root element of `UIComponent` and this viewport will be used for processing user input.

In order to render UIElement to a texture it is enough to call SetRenderTexture(texture), where texture is set up as `TEXTURE_RENDERTARGET`. In order to properly support input of `UIElement` rendered to a texture user should subclass `UIElement` and implement `ScreenToElement()` method which properly translates global screen coordinates to local element coordinates.

New API:
* UIElement::SetRenderTexture(Texture2D*)
* UI::SetRenderTexture(UIElement*, Texture2D*)
This commit is contained in:
Rokas Kupstys 2017-11-20 19:23:08 +02:00
parent 3b68cc6b64
commit c5c560e759
6 changed files with 188 additions and 88 deletions

View File

@ -441,32 +441,34 @@ void UI::RenderUpdate()
}
// Get batches for UI elements rendered into textures. Each element rendered into texture is treated as root element.
for (Vector<WeakPtr<UIComponent> >::Iterator it = renderToTexture_.Begin(); it != renderToTexture_.End();)
for (auto it = renderToTexture_.Begin(); it != renderToTexture_.End();)
{
WeakPtr<UIComponent> component = *it;
if (component.Null() || !component->IsEnabled())
RenderToTextureData& data = it->second_;
if (data.rootElement_.Expired())
it = renderToTexture_.Erase(it);
else if (component->IsEnabled())
else if (data.rootElement_->IsEnabled())
{
component->batches_.Clear();
component->vertexData_.Clear();
UIElement* element = component->GetRoot();
data.batches_.Clear();
data.vertexData_.Clear();
UIElement* element = data.rootElement_;
const IntVector2& size = element->GetSize();
const IntVector2& pos = element->GetPosition();
// Note: the scissors operate on unscaled coordinates. Scissor scaling is only performed during render
IntRect scissor = IntRect(pos.x_, pos.y_, pos.x_ + size.x_, pos.y_ + size.y_);
GetBatches(component->batches_, component->vertexData_, element, scissor);
GetBatches(data.batches_, data.vertexData_, element, scissor);
// UIElement does not have anything to show. Insert dummy batch that will clear the texture.
if (component->batches_.Empty())
if (data.batches_.Empty())
{
UIBatch batch(element, BLEND_REPLACE, scissor, nullptr, &component->vertexData_);
UIBatch batch(element, BLEND_REPLACE, scissor, nullptr, &data.vertexData_);
batch.SetColor(Color::BLACK);
batch.AddQuad(scissor.left_, scissor.top_, scissor.right_, scissor.bottom_, 0, 0);
component->batches_.Push(batch);
data.batches_.Push(batch);
}
++it;
}
else
++it;
}
}
@ -501,23 +503,23 @@ void UI::Render(bool renderUICommand)
// Render to UIComponent textures. This is skipped when called from the RENDERUI command
if (!renderUICommand)
{
for (Vector<WeakPtr<UIComponent> >::ConstIterator it = renderToTexture_.Begin(); it != renderToTexture_.End(); it++)
for (auto it = renderToTexture_.Begin(); it != renderToTexture_.End(); it++)
{
WeakPtr<UIComponent> component = *it;
if (component->IsEnabled())
RenderToTextureData& data = it->second_;
if (data.rootElement_->IsEnabled())
{
SetVertexData(component->vertexBuffer_, component->vertexData_);
SetVertexData(component->debugVertexBuffer_, component->debugVertexData_);
SetVertexData(data.vertexBuffer_, data.vertexData_);
SetVertexData(data.debugVertexBuffer_, data.debugVertexData_);
RenderSurface* surface = component->GetTexture()->GetRenderSurface();
RenderSurface* surface = data.texture_->GetRenderSurface();
graphics_->SetRenderTarget(0, surface);
graphics_->SetViewport(IntRect(0, 0, surface->GetWidth(), surface->GetHeight()));
graphics_->Clear(Urho3D::CLEAR_COLOR);
Render(component->vertexBuffer_, component->batches_, 0, component->batches_.Size());
Render(component->debugVertexBuffer_, component->debugDrawBatches_, 0, component->debugDrawBatches_.Size());
component->debugDrawBatches_.Clear();
component->debugVertexData_.Clear();
Render(data.vertexBuffer_, data.batches_, 0, data.batches_.Size());
Render(data.debugVertexBuffer_, data.debugDrawBatches_, 0, data.debugDrawBatches_.Size());
data.debugDrawBatches_.Clear();
data.debugVertexData_.Clear();
}
}
@ -546,12 +548,12 @@ void UI::DebugDraw(UIElement* element)
element->GetDebugDrawBatches(debugDrawBatches_, debugVertexData_, scissor);
else
{
for (Vector<WeakPtr<UIComponent> >::Iterator it = renderToTexture_.Begin(); it != renderToTexture_.End(); it++)
for (auto it = renderToTexture_.Begin(); it != renderToTexture_.End(); it++)
{
WeakPtr<UIComponent> component = *it;
if (component.NotNull() && component->GetRoot() == root && component->IsEnabled())
RenderToTextureData& data = it->second_;
if (!data.rootElement_.Expired() && data.rootElement_ == root && data.rootElement_->IsEnabled())
{
element->GetDebugDrawBatches(component->debugDrawBatches_, component->debugVertexData_, scissor);
element->GetDebugDrawBatches(data.debugDrawBatches_, data.debugVertexData_, scissor);
break;
}
}
@ -768,16 +770,16 @@ UIElement* UI::GetElementAt(const IntVector2& position, bool enabledOnly, IntVec
// Mouse was not hovering UI element. Check elements rendered on 3D objects.
if (!result && renderToTexture_.Size())
{
for (Vector<WeakPtr<UIComponent> >::Iterator it = renderToTexture_.Begin(); it != renderToTexture_.End(); it++)
for (auto it = renderToTexture_.Begin(); it != renderToTexture_.End(); it++)
{
WeakPtr<UIComponent> component = *it;
if (component.Null() || !component->IsEnabled())
RenderToTextureData& data = it->second_;
if (data.rootElement_.Expired() || !data.rootElement_->IsEnabled())
continue;
IntVector2 screenPosition;
if (component->ScreenToUIPosition(position, screenPosition))
IntVector2 screenPosition = data.rootElement_->ScreenToElement(position);
if (data.rootElement_->GetCombinedScreenRect().IsInside(screenPosition) == INSIDE)
{
result = GetElementAt(component->GetRoot(), screenPosition, enabledOnly);
result = GetElementAt(data.rootElement_, screenPosition, enabledOnly);
if (result)
{
if (elementScreenPosition)
@ -2076,16 +2078,31 @@ IntVector2 UI::GetEffectiveRootElementSize(bool applyScale) const
return size;
}
void UI::SetRenderToTexture(UIComponent* component, bool enable)
void UI::SetRenderTexture(UIElement* element, Texture2D* texture)
{
WeakPtr<UIComponent> weak(component);
if (enable)
if (element == nullptr)
{
if (!renderToTexture_.Contains(weak))
renderToTexture_.Push(weak);
URHO3D_LOGERROR("UI::SetRenderTexture called with null element.");
return;
}
auto it = renderToTexture_.Find(element);
if (texture && it == renderToTexture_.End())
{
RenderToTextureData data;
data.texture_ = texture;
data.rootElement_ = element;
data.vertexBuffer_ = new VertexBuffer(context_);
data.debugVertexBuffer_ = new VertexBuffer(context_);
renderToTexture_[element] = data;
}
else if (it != renderToTexture_.End())
{
if (texture == nullptr)
renderToTexture_.Erase(it);
else
it->second_.texture_ = texture;
}
else
renderToTexture_.Remove(weak);
}
void RegisterUILibrary(Context* context)
@ -2114,6 +2131,7 @@ void RegisterUILibrary(Context* context)
ProgressBar::RegisterObject(context);
ToolTip::RegisterObject(context);
UIComponent::RegisterObject(context);
UIElement3D::RegisterObject(context);
}
}

View File

@ -23,6 +23,7 @@
#pragma once
#include "../Core/Object.h"
#include "../Graphics/VertexBuffer.h"
#include "../UI/Cursor.h"
#include "../UI/UIBatch.h"
@ -48,7 +49,6 @@ class ResourceCache;
class Timer;
class UIBatch;
class UIElement;
class VertexBuffer;
class XMLElement;
class XMLFile;
class RenderSurface;
@ -212,8 +212,8 @@ public:
/// Return root element custom size. Returns 0,0 when custom size is not being used and automatic resizing according to window size is in use instead (default.)
const IntVector2& GetCustomSize() const { return customSize_; }
/// Register UIElement for being rendered into a texture.
void SetRenderToTexture(UIComponent* component, bool enable);
/// Set texture to which element will be rendered.
void SetRenderTexture(UIElement* element, Texture2D* texture);
/// Data structure used to represent the drag data associated to a UIElement.
struct DragData
@ -233,6 +233,27 @@ public:
};
private:
/// Data structured used to hold data of UI elements that are rendered to texture.
struct RenderToTextureData
{
/// UIElement to be rendered into texture.
WeakPtr<UIElement> rootElement_;
/// Texture that UIElement will be rendered into.
SharedPtr<Texture2D> texture_;
/// UI rendering batches.
PODVector<UIBatch> batches_;
/// UI rendering vertex data.
PODVector<float> vertexData_;
/// UI vertex buffer.
SharedPtr<VertexBuffer> vertexBuffer_;
/// UI rendering batches for debug draw.
PODVector<UIBatch> debugDrawBatches_;
/// UI rendering vertex data for debug draw.
PODVector<float> debugVertexData_;
/// UI debug geometry vertex buffer.
SharedPtr<VertexBuffer> debugVertexBuffer_;
};
/// Initialize when screen mode initially set.
void Initialize();
/// Update UI element logic recursively.
@ -398,9 +419,7 @@ private:
/// Root element custom size. 0,0 for automatic resizing (default.)
IntVector2 customSize_;
/// Elements that should be rendered to textures.
Vector<WeakPtr<UIComponent> > renderToTexture_;
friend class UIComponent;
HashMap<UIElement*, RenderToTextureData> renderToTexture_;
};
/// Register UI library objects.

View File

@ -45,16 +45,21 @@ static int const UICOMPONENT_DEFAULT_TEXTURE_SIZE = 512;
static int const UICOMPONENT_MIN_TEXTURE_SIZE = 64;
static int const UICOMPONENT_MAX_TEXTURE_SIZE = 4096;
UIComponent::UIComponent(Context* context) :
Component(context),
isStaticModelOwned_(false)
{
vertexBuffer_ = new VertexBuffer(context_);
debugVertexBuffer_ = new VertexBuffer(context_);
texture_ = context_->CreateObject<Texture2D>();
texture_->SetFilterMode(FILTER_BILINEAR);
texture_->SetAddressMode(COORD_U, ADDRESS_CLAMP);
texture_->SetAddressMode(COORD_V, ADDRESS_CLAMP);
texture_->SetNumLevels(1); // No mipmaps
rootElement_ = context_->CreateObject<UIElement>();
rootElement_ = context_->CreateObject<UIElement3D>();
rootElement_->SetUIComponent(this);
rootElement_->SetTraversalMode(TM_BREADTH_FIRST);
rootElement_->SetEnabled(true);
material_ = context_->CreateObject<Material>();
material_->SetTechnique(0, GetSubsystem<ResourceCache>()->GetResource<Technique>("Techniques/Diff.xml"));
@ -63,6 +68,7 @@ UIComponent::UIComponent(Context* context) :
SubscribeToEvent(rootElement_, E_RESIZED, URHO3D_HANDLER(UIComponent, OnElementResized));
// Triggers resizing of texture.
rootElement_->SetRenderTexture(texture_);
rootElement_->SetSize(UICOMPONENT_DEFAULT_TEXTURE_SIZE, UICOMPONENT_DEFAULT_TEXTURE_SIZE);
}
@ -90,6 +96,10 @@ Texture2D* UIComponent::GetTexture() const
return texture_;
}
StaticModel* UIComponent::GetModel() const
{
return model_;
}
void UIComponent::OnNodeSet(Node* node)
{
@ -102,9 +112,11 @@ void UIComponent::OnNodeSet(Node* node)
model_ = node->CreateComponent<StaticModel>();
}
model_->SetMaterial(material_);
rootElement_->SetRenderTexture(texture_);
}
else
{
rootElement_->SetRenderTexture(nullptr);
model_->SetMaterial(nullptr);
if (isStaticModelOwned_)
{
@ -113,11 +125,6 @@ void UIComponent::OnNodeSet(Node* node)
}
model_ = nullptr;
}
UI* ui = GetSubsystem<UI>();
// May be null on shutdown
if (ui)
ui->SetRenderToTexture(this, node != nullptr);
}
void UIComponent::OnElementResized(StringHash eventType, VariantMap& args)
@ -134,38 +141,42 @@ void UIComponent::OnElementResized(StringHash eventType, VariantMap& args)
}
if (texture_->SetSize(width, height, GetSubsystem<Graphics>()->GetRGBAFormat(), TEXTURE_RENDERTARGET))
{
texture_->SetFilterMode(FILTER_BILINEAR);
texture_->SetAddressMode(COORD_U, ADDRESS_CLAMP);
texture_->SetAddressMode(COORD_V, ADDRESS_CLAMP);
texture_->SetNumLevels(1); // No mipmaps
texture_->GetRenderSurface()->SetUpdateMode(SURFACE_MANUALUPDATE);
}
else
URHO3D_LOGERROR("UIComponent: resizing texture failed.");
}
bool UIComponent::ScreenToUIPosition(IntVector2 screenPos, IntVector2& result)
IntVector2 UIElement3D::ScreenToElement(const IntVector2& screenPos)
{
Scene* scene = GetScene();
IntVector2 result(-1, -1);
Scene* scene = component_->GetScene();
if (!scene)
return false;
return result;
Renderer* renderer = GetSubsystem<Renderer>();
if (!renderer)
return false;
return result;
// \todo Always uses the first viewport, in case there are multiple
Viewport* viewport = renderer->GetViewportForScene(scene, 0);
Octree* octree = scene->GetComponent<Octree>();
Viewport* viewport = viewport_;
if (viewport == nullptr)
viewport = renderer->GetViewportForScene(scene, 0);
if (!viewport || !octree)
return false;
return result;
if (viewport->GetScene() != scene)
{
URHO3D_LOGERROR("UIComponent and Viewport set to component's root element belong to different scenes.");
return result;
}
Camera* camera = viewport->GetCamera();
if (!camera)
return false;
return result;
IntRect rect = viewport->GetRect();
if (rect == IntRect::ZERO)
@ -182,25 +193,51 @@ bool UIComponent::ScreenToUIPosition(IntVector2 screenPos, IntVector2& result)
octree->Raycast(query);
if (queryResultVector.Empty())
return false;
return result;
for (unsigned i = 0; i < queryResultVector.Size(); i++)
{
RayQueryResult& queryResult = queryResultVector[i];
if (queryResult.drawable_ != model_)
if (queryResult.drawable_ != component_->GetModel())
{
// ignore billboard sets by default
if (queryResult.drawable_->GetTypeInfo()->IsTypeOf(BillboardSet::GetTypeStatic()))
continue;
return false;
return result;
}
Vector2& uv = queryResult.textureUV_;
result = IntVector2(static_cast<int>(uv.x_ * rootElement_->GetWidth()),
static_cast<int>(uv.y_ * rootElement_->GetHeight()));
return true;
result = IntVector2(static_cast<int>(uv.x_ * component_->GetRoot()->GetWidth()),
static_cast<int>(uv.y_ * component_->GetRoot()->GetHeight()));
return result;
}
return false;
return result;
}
IntVector2 UIElement3D::ElementToScreen(const IntVector2& position)
{
URHO3D_LOGERROR("UIElement3D::ElementToScreen is not implemented.");
return {-1, -1};
}
void UIElement3D::RegisterObject(Context* context)
{
context->RegisterFactory<UIElement3D>();
}
UIElement3D::UIElement3D(Context* context)
: UIElement(context)
{
}
void UIElement3D::SetUIComponent(UIComponent* component)
{
component_ = component;
}
void UIElement3D::SetViewport(Viewport* viewport)
{
viewport_ = viewport;
}
}

View File

@ -22,6 +22,7 @@
#pragma once
#include "../Scene/Component.h"
#include "../UI/UIElement.h"
namespace Urho3D
{
@ -33,6 +34,7 @@ class Viewport;
class UIElement;
class UIBatch;
class VertexBuffer;
class UIElement3D;
class URHO3D_API UIComponent : public Component
{
@ -52,6 +54,8 @@ public:
Material* GetMaterial() const;
/// Return texture which will be used for rendering UI to.
Texture2D* GetTexture() const;
/// Return static model on to which UI will be rendered.
StaticModel* GetModel() const;
protected:
/// Material that is set to the model.
@ -61,29 +65,41 @@ protected:
/// Model that texture will be applied to.
SharedPtr<StaticModel> model_;
/// UIElement to be rendered into texture.
SharedPtr<UIElement> rootElement_;
/// UI rendering batches.
PODVector<UIBatch> batches_;
/// UI rendering vertex data.
PODVector<float> vertexData_;
/// UI vertex buffer.
SharedPtr<VertexBuffer> vertexBuffer_;
/// UI rendering batches for debug draw.
PODVector<UIBatch> debugDrawBatches_;
/// UI rendering vertex data for debug draw.
PODVector<float> debugVertexData_;
/// UI debug geometry vertex buffer.
SharedPtr<VertexBuffer> debugVertexBuffer_;
SharedPtr<UIElement3D> rootElement_;
/// Is StaticModel component created by this component.
bool isStaticModelOwned_;
/// Handle component being added to Node or removed from it.
virtual void OnNodeSet(Node* node) override;
/// Handle resizing of element. Setting size of element will automatically resize texture. UIElement size matches size of texture.
void OnElementResized(StringHash eventType, VariantMap& args);
/// Convert screen position to position on UIElement.
bool ScreenToUIPosition(IntVector2 screenPos, IntVector2& result);
};
friend class UI;
class URHO3D_API UIElement3D : public UIElement
{
URHO3D_OBJECT(UIElement3D, UIElement);
public:
/// Construct.
UIElement3D(Context* context);
/// Destruct.
virtual ~UIElement3D() override = default;
/// Register object factory.
static void RegisterObject(Context* context);
/// Set UIComponent which is using this element as root element.
void SetUIComponent(UIComponent* component);
/// Set active viewport through which this element is rendered. If viewport is not set, it defaults to first viewport.
void SetViewport(Viewport* viewport);
/// Convert screen coordinates to element coordinates.
IntVector2 ScreenToElement(const IntVector2& screenPos) override;
/// Convert element coordinates to screen coordinates.
IntVector2 ElementToScreen(const IntVector2& position) override;
protected:
/// A UIComponent which owns this element.
WeakPtr<UIComponent> component_;
/// Viewport which renders this element.
WeakPtr<Viewport> viewport_;
};
}

View File

@ -2277,4 +2277,10 @@ void UIElement::HandlePostUpdate(StringHash eventType, VariantMap& eventData)
UpdateAttributeAnimations(eventData[P_TIMESTEP].GetFloat());
}
void UIElement::SetRenderTexture(Texture2D* texture)
{
if (UI* ui = GetSubsystem<UI>())
ui->SetRenderTexture(this, texture);
}
}

View File

@ -109,6 +109,7 @@ static const unsigned DD_SOURCE_AND_TARGET = 0x3;
class Cursor;
class ResourceCache;
class Texture2D;
/// Base class for %UI elements.
class URHO3D_API UIElement : public Animatable
@ -640,6 +641,9 @@ public:
/// Return effective minimum size, also considering layout. Used internally.
IntVector2 GetEffectiveMinSize() const;
/// Set texture to which element will be rendered.
void SetRenderTexture(Texture2D* texture);
protected:
/// Handle attribute animation added.
virtual void OnAttributeAnimationAdded() override;