From c76d267c2abeace5f4d6fed9a13e19a7b10512c0 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 28 Jun 2019 13:53:44 -0700 Subject: [PATCH 1/3] separate text into two render items --- .../src/RenderableTextEntityItem.cpp | 275 ++++++++++++++---- .../src/RenderableTextEntityItem.h | 42 ++- 2 files changed, 253 insertions(+), 64 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index a281c1d097..381edd8731 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -25,28 +25,22 @@ using namespace render; using namespace render::entities; static const int FIXED_FONT_POINT_SIZE = 40; -const int FIXED_FONT_SCALING_RATIO = FIXED_FONT_POINT_SIZE * 92.0f; // Determined through experimentation to fit font to line - // height. +const int FIXED_FONT_SCALING_RATIO = FIXED_FONT_POINT_SIZE * 92.0f; // Determined through experimentation to fit font to line height. const float LINE_SCALE_RATIO = 1.2f; TextEntityRenderer::TextEntityRenderer(const EntityItemPointer& entity) : Parent(entity), _textRenderer(TextRenderer3D::getInstance(SANS_FONT_FAMILY, FIXED_FONT_POINT_SIZE / 2.0f)) { - auto geometryCache = DependencyManager::get(); - if (geometryCache) { - _geometryID = geometryCache->allocateID(); - } -} - -TextEntityRenderer::~TextEntityRenderer() { - auto geometryCache = DependencyManager::get(); - if (_geometryID && geometryCache) { - geometryCache->releaseID(_geometryID); - } } bool TextEntityRenderer::isTransparent() const { - return Parent::isTransparent() || _textAlpha < 1.0f || _backgroundAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; + return Parent::isTransparent() || _backgroundAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; +} + +bool TextEntityRenderer::isTextTransparent() const { + return resultWithReadLock([&] { + return Parent::isTransparent() || _textAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; + }); } Item::Bound TextEntityRenderer::getBound() { @@ -61,7 +55,7 @@ Item::Bound TextEntityRenderer::getBound() { } ShapeKey TextEntityRenderer::getShapeKey() { - auto builder = render::ShapeKey::Builder().withOwnPipeline(); + auto builder = render::ShapeKey::Builder(); if (isTransparent()) { builder.withTranslucent(); } @@ -134,6 +128,7 @@ void TextEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen _dimensions = entity->getScaledDimensions(); updateModelTransformAndBound(); _renderTransform = getModelTransform(); + _renderTransform.postScale(_dimensions); }); }); } @@ -152,65 +147,49 @@ void TextEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointe _rightMargin = entity->getRightMargin(); _topMargin = entity->getTopMargin(); _bottomMargin = entity->getBottomMargin(); + updateTextRenderItem(); }); } void TextEntityRenderer::doRender(RenderArgs* args) { PerformanceTimer perfTimer("RenderableTextEntityItem::render"); - - glm::vec4 textColor; - glm::vec4 backgroundColor; - Transform modelTransform; - glm::vec3 dimensions; - BillboardMode billboardMode; - bool forward; - withReadLock([&] { - modelTransform = _renderTransform; - dimensions = _dimensions; - billboardMode = _billboardMode; - - float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - textColor = glm::vec4(_textColor, fadeRatio * _textAlpha); - textColor = EntityRenderer::calculatePulseColor(textColor, _pulseProperties, _created); - backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha); - backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created); - forward = _renderLayer != RenderLayer::WORLD || args->_renderMethod == render::Args::FORWARD; - }); - - // Render background - static const float SLIGHTLY_BEHIND = -0.005f; - glm::vec3 minCorner = glm::vec3(0.0f, -dimensions.y, SLIGHTLY_BEHIND); - glm::vec3 maxCorner = glm::vec3(dimensions.x, 0.0f, SLIGHTLY_BEHIND); - - // Batch render calls Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; - // FIXME: we need to find a better way of rendering text so we don't have to do this - if (forward) { - DependencyManager::get()->setupKeyLightBatch(args, batch); + glm::vec4 backgroundColor; + Transform modelTransform; + BillboardMode billboardMode; + PrimitiveMode primitiveMode; + RenderLayer renderLayer; + withReadLock([&] { + modelTransform = _renderTransform; + billboardMode = _billboardMode; + primitiveMode = _primitiveMode; + renderLayer = _renderLayer; + + float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; + backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha); + backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created); + }); + + if (backgroundColor.a <= 0.0f) { + return; } - auto transformToTopLeft = modelTransform; - transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), billboardMode, args->getViewFrustum().getPosition())); - transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left - transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed + modelTransform.setRotation(EntityItem::getBillboardRotation(modelTransform.getTranslation(), modelTransform.getRotation(), billboardMode, args->getViewFrustum().getPosition())); + batch.setModelTransform(modelTransform); - if (backgroundColor.a > 0.0f) { - batch.setModelTransform(transformToTopLeft); - auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, forward); - geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); + auto geometryCache = DependencyManager::get(); + render::ShapePipelinePointer pipeline; + if (renderLayer == RenderLayer::WORLD && args->_renderMethod != Args::RenderMethod::FORWARD) { + pipeline = backgroundColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); + } else { + pipeline = backgroundColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); } - - if (textColor.a > 0.0f) { - // FIXME: Factor out textRenderer so that text parts can be grouped by pipeline for a gpu performance increase. - float scale = _lineHeight / _textRenderer->getFontSize(); - transformToTopLeft.setScale(scale); // Scale to have the correct line height - batch.setModelTransform(transformToTopLeft); - - glm::vec2 bounds = glm::vec2(dimensions.x - (_leftMargin + _rightMargin), dimensions.y - (_topMargin + _bottomMargin)); - _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, forward); + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) { + geometryCache->renderWireShapeInstance(args, batch, GeometryCache::Quad, backgroundColor, pipeline); + } else { + geometryCache->renderSolidShapeInstance(args, batch, GeometryCache::Quad, backgroundColor, pipeline); } } @@ -222,4 +201,176 @@ QSizeF TextEntityRenderer::textSize(const QString& text) const { float pointToWorldScale = (maxHeight / FIXED_FONT_SCALING_RATIO) * _lineHeight; return QSizeF(extents.x, extents.y) * pointToWorldScale; +} + +void TextEntityRenderer::onAddToSceneTyped(const TypedEntityPointer& entity) { + Parent::onAddToSceneTyped(entity); + _textPayload = std::make_shared(entity->getID(), _textRenderer); + _textRenderID = AbstractViewStateInterface::instance()->getMain3DScene()->allocateID(); + auto renderPayload = std::make_shared(_textPayload); + render::Transaction transaction; + transaction.resetItem(_textRenderID, renderPayload); + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); + updateTextRenderItem(); +} + +void TextEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { + Parent::onRemoveFromSceneTyped(entity); + render::Transaction transaction; + transaction.removeItem(_textRenderID); + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); + _textPayload.reset(); +} + +void TextEntityRenderer::updateTextRenderItem() const { + render::Transaction transaction; + transaction.updateItem(_textRenderID); + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); +} + +entities::TextPayload::TextPayload(const QUuid& entityID, const std::weak_ptr& textRenderer) : + _entityID(entityID), _textRenderer(textRenderer) { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + _geometryID = geometryCache->allocateID(); + } +} + +entities::TextPayload::~TextPayload() { + auto geometryCache = DependencyManager::get(); + if (_geometryID && geometryCache) { + geometryCache->releaseID(_geometryID); + } +} + +ItemKey entities::TextPayload::getKey() const { + auto renderable = DependencyManager::get()->renderableForEntityId(_entityID); + if (renderable) { + auto textRenderable = std::static_pointer_cast(renderable); + ItemKey::Builder key; + // Similar to EntityRenderer::getKey() + // FIXME: should be withSubMetaCulled and not meta, but there's a bug in the layer rendering that won't render it in that case + // (the TextEntityRenderer should also be withMetaCullGroup) + if (textRenderable->isTextTransparent()) { + key = ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(textRenderable->getTagMask()).withLayer(textRenderable->getHifiRenderLayer()); + } else if (textRenderable->_canCastShadow) { + key = ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(textRenderable->getTagMask()).withShadowCaster().withLayer(textRenderable->getHifiRenderLayer()); + } else { + key = ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(textRenderable->getTagMask()).withLayer(textRenderable->getHifiRenderLayer()); + } + + if (!textRenderable->_visible) { + key.withInvisible(); + } + return key; + } + return ItemKey::Builder::opaqueShape(); +} + +Item::Bound entities::TextPayload::getBound() const { + auto renderable = DependencyManager::get()->renderableForEntityId(_entityID); + if (renderable) { + return std::static_pointer_cast(renderable)->getBound(); + } + return Item::Bound(); +} + +ShapeKey entities::TextPayload::getShapeKey() const { + auto renderable = DependencyManager::get()->renderableForEntityId(_entityID); + if (renderable) { + auto textRenderable = std::static_pointer_cast(renderable); + + auto builder = render::ShapeKey::Builder().withOwnPipeline(); + if (textRenderable->isTextTransparent()) { + builder.withTranslucent(); + } + if (textRenderable->_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); + } + return ShapeKey::Builder::invalid(); +} + +void entities::TextPayload::render(RenderArgs* args) { + PerformanceTimer perfTimer("TextPayload::render"); + Q_ASSERT(args->_batch); + gpu::Batch& batch = *args->_batch; + + auto textRenderer = _textRenderer.lock(); + if (!textRenderer) { + return; + } + + auto renderable = DependencyManager::get()->renderableForEntityId(_entityID); + if (!renderable) { + return; + } + auto textRenderable = std::static_pointer_cast(renderable); + + glm::vec4 textColor; + Transform modelTransform; + BillboardMode billboardMode; + float lineHeight, leftMargin, rightMargin, topMargin, bottomMargin; + QString text; + glm::vec3 dimensions; + bool forward; + textRenderable->withReadLock([&] { + modelTransform = textRenderable->_renderTransform; + billboardMode = textRenderable->_billboardMode; + lineHeight = textRenderable->_lineHeight; + leftMargin = textRenderable->_leftMargin; + rightMargin = textRenderable->_rightMargin; + topMargin = textRenderable->_topMargin; + bottomMargin = textRenderable->_bottomMargin; + text = textRenderable->_text; + dimensions = textRenderable->_dimensions; + + float fadeRatio = textRenderable->_isFading ? Interpolate::calculateFadeRatio(textRenderable->_fadeStartTime) : 1.0f; + textColor = glm::vec4(textRenderable->_textColor, fadeRatio * textRenderable->_textAlpha); + textColor = EntityRenderer::calculatePulseColor(textColor, textRenderable->_pulseProperties, textRenderable->_created); + forward = textRenderable->_renderLayer != RenderLayer::WORLD || args->_renderMethod == render::Args::FORWARD; + }); + + if (textColor.a <= 0.0f) { + return; + } + + modelTransform.setRotation(EntityItem::getBillboardRotation(modelTransform.getTranslation(), modelTransform.getRotation(), billboardMode, args->getViewFrustum().getPosition())); + + float scale = lineHeight / textRenderer->getFontSize(); + const float TEXT_ENTITY_ITEM_FIXED_DEPTH = 0.01f; + modelTransform.postTranslate(glm::vec3(-0.5, 0.5, 1.0f + EPSILON / dimensions.z)); + modelTransform.setScale(scale); + batch.setModelTransform(modelTransform); + + glm::vec2 bounds = glm::vec2(dimensions.x - (leftMargin + rightMargin), dimensions.y - (topMargin + bottomMargin)); + textRenderer->draw(batch, leftMargin / scale, -topMargin / scale, text, textColor, bounds / scale, forward); +} + +namespace render { +template <> const ItemKey payloadGetKey(const TextPayload::Pointer& payload) { + if (payload) { + return payload->getKey(); + } + return ItemKey::Builder::opaqueShape(); +} + +template <> const Item::Bound payloadGetBound(const TextPayload::Pointer& payload) { + if (payload) { + return payload->getBound(); + } + return Item::Bound(); +} + +template <> const ShapeKey shapeGetShapeKey(const TextPayload::Pointer& payload) { + if (payload) { + return payload->getShapeKey(); + } + return ShapeKey::Builder::invalid(); +} + +template <> void payloadRender(const TextPayload::Pointer& payload, RenderArgs* args) { + return payload->render(args); +} } \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index e0306375a0..04b1c7a851 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -19,27 +19,31 @@ class TextRenderer3D; namespace render { namespace entities { +class TextPayload; + class TextEntityRenderer : public TypedEntityRenderer { using Parent = TypedEntityRenderer; using Pointer = std::shared_ptr; public: TextEntityRenderer(const EntityItemPointer& entity); - ~TextEntityRenderer(); QSizeF textSize(const QString& text) const; protected: bool isTransparent() const override; + bool isTextTransparent() const; Item::Bound getBound() override; ShapeKey getShapeKey() override; + void onAddToSceneTyped(const TypedEntityPointer& entity) override; + void onRemoveFromSceneTyped(const TypedEntityPointer& entity) override; + private: virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; - int _geometryID{ 0 }; std::shared_ptr _textRenderer; PulsePropertyGroup _pulseProperties; @@ -58,8 +62,42 @@ private: BillboardMode _billboardMode; glm::vec3 _dimensions; + + std::shared_ptr _textPayload; + render::ItemID _textRenderID; + void updateTextRenderItem() const; + + friend class render::entities::TextPayload; +}; + +class TextPayload { +public: + TextPayload() = default; + TextPayload(const QUuid& entityID, const std::weak_ptr& textRenderer); + ~TextPayload(); + + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + + ItemKey getKey() const; + Item::Bound getBound() const; + ShapeKey getShapeKey() const; + void render(RenderArgs* args); + +protected: + QUuid _entityID; + std::weak_ptr _textRenderer; + + int _geometryID { 0 }; }; } } +namespace render { + template <> const ItemKey payloadGetKey(const entities::TextPayload::Pointer& payload); + template <> const Item::Bound payloadGetBound(const entities::TextPayload::Pointer& payload); + template <> const ShapeKey shapeGetShapeKey(const entities::TextPayload::Pointer& payload); + template <> void payloadRender(const entities::TextPayload::Pointer& payload, RenderArgs* args); +} + #endif // hifi_RenderableTextEntityItem_h From a5b92a6c7b67fee641b25ebd0453a8f3f415328a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 28 Jun 2019 14:24:33 -0700 Subject: [PATCH 2/3] fix layered rendering of group culled sub items --- .../src/RenderableTextEntityItem.cpp | 21 ++++++++++++++----- .../src/RenderableTextEntityItem.h | 2 ++ .../src/render/RenderFetchCullSortTask.cpp | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 381edd8731..c9d6df8c8f 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -54,6 +54,10 @@ Item::Bound TextEntityRenderer::getBound() { return bound; } +ItemKey TextEntityRenderer::getKey() { + return ItemKey::Builder(Parent::getKey()).withMetaCullGroup(); +} + ShapeKey TextEntityRenderer::getShapeKey() { auto builder = render::ShapeKey::Builder(); if (isTransparent()) { @@ -65,6 +69,15 @@ ShapeKey TextEntityRenderer::getShapeKey() { return builder.build(); } +uint32_t TextEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) { + auto parentSubs = Parent::metaFetchMetaSubItems(subItems); + if (Item::isValidID(_textRenderID)) { + subItems.emplace_back(_textRenderID); + return parentSubs + 1; + } + return parentSubs; +} + bool TextEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { if (_text != entity->getText()) { return true; @@ -249,14 +262,12 @@ ItemKey entities::TextPayload::getKey() const { auto textRenderable = std::static_pointer_cast(renderable); ItemKey::Builder key; // Similar to EntityRenderer::getKey() - // FIXME: should be withSubMetaCulled and not meta, but there's a bug in the layer rendering that won't render it in that case - // (the TextEntityRenderer should also be withMetaCullGroup) if (textRenderable->isTextTransparent()) { - key = ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(textRenderable->getTagMask()).withLayer(textRenderable->getHifiRenderLayer()); + key = ItemKey::Builder::transparentShape().withSubMetaCulled().withTagBits(textRenderable->getTagMask()).withLayer(textRenderable->getHifiRenderLayer()); } else if (textRenderable->_canCastShadow) { - key = ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(textRenderable->getTagMask()).withShadowCaster().withLayer(textRenderable->getHifiRenderLayer()); + key = ItemKey::Builder::opaqueShape().withSubMetaCulled().withTagBits(textRenderable->getTagMask()).withShadowCaster().withLayer(textRenderable->getHifiRenderLayer()); } else { - key = ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(textRenderable->getTagMask()).withLayer(textRenderable->getHifiRenderLayer()); + key = ItemKey::Builder::opaqueShape().withSubMetaCulled().withTagBits(textRenderable->getTagMask()).withLayer(textRenderable->getHifiRenderLayer()); } if (!textRenderable->_visible) { diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index 04b1c7a851..d5e36a4622 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -34,6 +34,8 @@ protected: bool isTextTransparent() const; Item::Bound getBound() override; ShapeKey getShapeKey() override; + ItemKey getKey() override; + virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; void onAddToSceneTyped(const TypedEntityPointer& entity) override; void onRemoveFromSceneTyped(const TypedEntityPointer& entity) override; diff --git a/libraries/render/src/render/RenderFetchCullSortTask.cpp b/libraries/render/src/render/RenderFetchCullSortTask.cpp index d82fdef258..6b1a57ed88 100644 --- a/libraries/render/src/render/RenderFetchCullSortTask.cpp +++ b/libraries/render/src/render/RenderFetchCullSortTask.cpp @@ -30,7 +30,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin const auto culledSpatialSelection = task.addJob("CullSceneSelection", cullInputs, cullFunctor, RenderDetails::ITEM); // Layered objects are not culled - const ItemFilter layeredFilter = ItemFilter::Builder().withVisible().withoutSubMetaCulled().withTagBits(tagBits, tagMask); + const ItemFilter layeredFilter = ItemFilter::Builder::visibleWorldItems().withTagBits(tagBits, tagMask); const auto nonspatialFilter = render::Varying(layeredFilter); const auto nonspatialSelection = task.addJob("FetchLayeredSelection", nonspatialFilter); From e2bf3d7138d0aa4b698e34ac36efa94f1e7f6fde Mon Sep 17 00:00:00 2001 From: Sam Gondelman Date: Fri, 28 Jun 2019 15:11:22 -0700 Subject: [PATCH 3/3] remove unused constant --- libraries/entities-renderer/src/RenderableTextEntityItem.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index c9d6df8c8f..d871df78d5 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -350,7 +350,6 @@ void entities::TextPayload::render(RenderArgs* args) { modelTransform.setRotation(EntityItem::getBillboardRotation(modelTransform.getTranslation(), modelTransform.getRotation(), billboardMode, args->getViewFrustum().getPosition())); float scale = lineHeight / textRenderer->getFontSize(); - const float TEXT_ENTITY_ITEM_FIXED_DEPTH = 0.01f; modelTransform.postTranslate(glm::vec3(-0.5, 0.5, 1.0f + EPSILON / dimensions.z)); modelTransform.setScale(scale); batch.setModelTransform(modelTransform); @@ -384,4 +383,4 @@ template <> const ShapeKey shapeGetShapeKey(const TextPayload::Pointer& payload) template <> void payloadRender(const TextPayload::Pointer& payload, RenderArgs* args) { return payload->render(args); } -} \ No newline at end of file +}