2
0
Yusuf Umar 9 жил өмнө
parent
commit
f9e65027e3
31 өөрчлөгдсөн 2221 нэмэгдсэн , 15 устгасан
  1. 33 0
      Source/Samples/44_RibbonTrailDemo/CMakeLists.txt
  2. 279 0
      Source/Samples/44_RibbonTrailDemo/RibbonTrailDemo.cpp
  3. 82 0
      Source/Samples/44_RibbonTrailDemo/RibbonTrailDemo.h
  4. 40 0
      Source/Urho3D/AngelScript/GraphicsAPI.cpp
  5. 2 0
      Source/Urho3D/Graphics/Direct3D11/D3D11Graphics.cpp
  6. 2 0
      Source/Urho3D/Graphics/Direct3D9/D3D9Graphics.cpp
  7. 4 2
      Source/Urho3D/Graphics/GraphicsDefs.h
  8. 2 0
      Source/Urho3D/Graphics/OpenGL/OGLGraphics.cpp
  9. 3 1
      Source/Urho3D/Graphics/Renderer.cpp
  10. 897 0
      Source/Urho3D/Graphics/RibbonTrail.cpp
  11. 242 0
      Source/Urho3D/Graphics/RibbonTrail.h
  12. 54 0
      Source/Urho3D/LuaScript/pkgs/Graphics/RibbonTrail.pkg
  13. 1 0
      Source/Urho3D/LuaScript/pkgs/GraphicsLuaAPI.pkg
  14. 37 0
      bin/CoreData/Shaders/GLSL/Transform.glsl
  15. 4 0
      bin/CoreData/Shaders/GLSL/Unlit.glsl
  16. 3 1
      bin/CoreData/Shaders/HLSL/Basic.hlsl
  17. 2 2
      bin/CoreData/Shaders/HLSL/LitParticle.hlsl
  18. 2 2
      bin/CoreData/Shaders/HLSL/LitSolid.hlsl
  19. 2 2
      bin/CoreData/Shaders/HLSL/PBRLitSolid.hlsl
  20. 1 1
      bin/CoreData/Shaders/HLSL/TerrainBlend.hlsl
  21. 37 0
      bin/CoreData/Shaders/HLSL/Transform.hlsl
  22. 3 1
      bin/CoreData/Shaders/HLSL/Unlit.hlsl
  23. 2 2
      bin/CoreData/Shaders/HLSL/Vegetation.hlsl
  24. 222 0
      bin/Data/LuaScripts/44_RibbonTrailDemo.lua
  25. 14 0
      bin/Data/Materials/RibbonTrail.xml
  26. 14 0
      bin/Data/Materials/SlashTrail.xml
  27. 228 0
      bin/Data/Scripts/44_RibbonTrailDemo.as
  28. 5 1
      bin/Data/Scripts/Editor/EditorSecondaryToolbar.as
  29. 0 0
      bin/Data/Textures/RibbonTrail.png
  30. 0 0
      bin/Data/Textures/SlashTrail.png
  31. 4 0
      bin/Data/UI/EditorIcons.xml

+ 33 - 0
Source/Samples/44_RibbonTrailDemo/CMakeLists.txt

@@ -0,0 +1,33 @@
+#
+# Copyright (c) 2008-2016 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.
+#
+
+# Define target name
+set (TARGET_NAME 44_RibbonTrailDemo)
+
+# Define source files
+define_source_files (EXTRA_H_FILES ${COMMON_SAMPLE_H_FILES})
+
+# Setup target with resource copying
+setup_main_executable ()
+
+# Setup test cases
+setup_test ()

+ 279 - 0
Source/Samples/44_RibbonTrailDemo/RibbonTrailDemo.cpp

@@ -0,0 +1,279 @@
+//
+// Copyright (c) 2008-2016 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 <Urho3D/Core/CoreEvents.h>
+#include <Urho3D/Engine/Engine.h>
+#include <Urho3D/Graphics/AnimatedModel.h>
+//#include <Urho3D/Graphics/Animation.h>
+//#include <Urho3D/Graphics/AnimationState.h>
+#include <Urho3D/Graphics/AnimationController.h>
+#include <Urho3D/Graphics/Camera.h>
+#include <Urho3D/Graphics/Graphics.h>
+#include <Urho3D/Graphics/Material.h>
+#include <Urho3D/Graphics/Model.h>
+#include <Urho3D/Graphics/Octree.h>
+#include <Urho3D/Graphics/Renderer.h>
+#include <Urho3D/Graphics/RibbonTrail.h>
+#include <Urho3D/Graphics/StaticModel.h>
+#include <Urho3D/Input/Input.h>
+#include <Urho3D/Resource/ResourceCache.h>
+#include <Urho3D/Scene/Scene.h>
+#include <Urho3D/UI/Font.h>
+#include <Urho3D/UI/Text.h>
+#include <Urho3D/UI/UI.h>
+#include <Urho3D/UI/Text3D.h>
+
+#include "RibbonTrailDemo.h"
+
+#include <Urho3D/DebugNew.h>
+
+URHO3D_DEFINE_APPLICATION_MAIN(RibbonTrailDemo)
+
+RibbonTrailDemo::RibbonTrailDemo(Context* context) :
+    Sample(context),
+    swordTrailStartTime_(0.2f),
+    swordTrailEndTime_(0.46f),
+    timeStepSum_(0.0f)
+{
+}
+
+void RibbonTrailDemo::Start()
+{
+    // Execute base class startup
+    Sample::Start();
+
+    // Create the scene content
+    CreateScene();
+
+    // Create the UI content
+    CreateInstructions();
+
+    // Setup the viewport for displaying the scene
+    SetupViewport();
+
+    // Hook up to the frame update events
+    SubscribeToEvents();
+
+    // Set the mouse mode to use in the sample
+    Sample::InitMouseMode(MM_RELATIVE);
+}
+
+void RibbonTrailDemo::CreateScene()
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+
+    scene_ = new Scene(context_);
+
+    // Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000)
+    scene_->CreateComponent<Octree>();
+
+    // Create scene node & StaticModel component for showing a static plane
+    Node* planeNode = scene_->CreateChild("Plane");
+    planeNode->SetScale(Vector3(100.0f, 1.0f, 100.0f));
+    StaticModel* planeObject = planeNode->CreateComponent<StaticModel>();
+    planeObject->SetModel(cache->GetResource<Model>("Models/Plane.mdl"));
+    planeObject->SetMaterial(cache->GetResource<Material>("Materials/StoneTiled.xml"));
+
+    // Create a directional light to the world.
+    Node* lightNode = scene_->CreateChild("DirectionalLight");
+    lightNode->SetDirection(Vector3(0.6f, -1.0f, 0.8f)); // The direction vector does not need to be normalized
+    Light* light = lightNode->CreateComponent<Light>();
+    light->SetLightType(LIGHT_DIRECTIONAL);
+    light->SetCastShadows(true);
+    light->SetShadowBias(BiasParameters(0.00005f, 0.5f));
+    // Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
+    light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f));
+
+    // Create first box for face camera trail demo with 1 column.
+    boxNode1_ = scene_->CreateChild("Box1");
+    StaticModel* box1 = boxNode1_->CreateComponent<StaticModel>();
+    box1->SetModel(cache->GetResource<Model>("Models/Box.mdl"));
+    box1->SetCastShadows(true);
+    RibbonTrail* boxTrail1 = boxNode1_->CreateComponent<RibbonTrail>();
+    boxTrail1->SetMaterial(cache->GetResource<Material>("Materials/RibbonTrail.xml"));
+    boxTrail1->SetStartColor(Color(1.0f, 0.5f, 0.0f, 1.0f));
+    boxTrail1->SetEndColor(Color(1.0f, 1.0f, 0.0f, 0.0f));
+    boxTrail1->SetWidth(0.5f);
+
+    // Create second box for face camera trail demo with 4 column.
+    // This will produce less distortion than first trail.
+    boxNode2_ = scene_->CreateChild("Box2");
+    StaticModel* box2 = boxNode2_->CreateComponent<StaticModel>();
+    box2->SetModel(cache->GetResource<Model>("Models/Box.mdl"));
+    box2->SetCastShadows(true);
+    RibbonTrail* boxTrail2 = boxNode2_->CreateComponent<RibbonTrail>();
+    boxTrail2->SetMaterial(cache->GetResource<Material>("Materials/RibbonTrail.xml"));
+    boxTrail2->SetStartColor(Color(1.0f, 0.5f, 0.0f, 1.0f));
+    boxTrail2->SetEndColor(Color(1.0f, 1.0f, 0.0f, 0.0f));
+    boxTrail2->SetWidth(0.5f);
+    boxTrail2->SetTailColumn(4);
+
+    // Load ninja animated model for bone trail demo.
+    Node* ninjaNode = scene_->CreateChild("Ninja");
+    ninjaNode->SetPosition(Vector3(5.0f, 0.0f, 0.0f));
+    ninjaNode->SetRotation(Quaternion(0.0f, 180.0f, 0.0f));
+    AnimatedModel* ninja = ninjaNode->CreateComponent<AnimatedModel>();
+    ninja->SetModel(cache->GetResource<Model>("Models/NinjaSnowWar/Ninja.mdl"));
+    ninja->SetMaterial(cache->GetResource<Material>("Materials/NinjaSnowWar/Ninja.xml"));
+    ninja->SetCastShadows(true);
+
+    // Create animation controller and play attack animation.
+    ninjaAnimCtrl_ = ninjaNode->CreateComponent<AnimationController>();
+    ninjaAnimCtrl_->PlayExclusive("Models/NinjaSnowWar/Ninja_Attack3.ani", 0, true, 0.0f);
+
+    // Add ribbon trail to tip of sword.
+    Node* swordTip = ninjaNode->GetChild("Joint29", true);
+    swordTrail_ = swordTip->CreateComponent<RibbonTrail>();
+
+    // Set sword trail type to bone and set other parameters.
+    swordTrail_->SetTrailType(TT_BONE);
+    swordTrail_->SetMaterial(cache->GetResource<Material>("Materials/SlashTrail.xml"));
+    swordTrail_->SetLifetime(0.22f);
+    swordTrail_->SetStartColor(Color(1.0f, 1.0f, 1.0f, 0.75f));
+    swordTrail_->SetEndColor(Color(0.2, 0.5f, 1.0f, 0.0f));
+    swordTrail_->SetTailColumn(4);
+
+    // Add floating text for info.
+    Node* boxTextNode1 = scene_->CreateChild("BoxText1");
+    boxTextNode1->SetPosition(Vector3(-1.0f, 2.0f, 0.0f));
+    Text3D* boxText1 = boxTextNode1->CreateComponent<Text3D>();
+    boxText1->SetText(String("Face Camera Trail (4 Column)"));
+    boxText1->SetFont(cache->GetResource<Font>("Fonts/BlueHighway.sdf"), 24);
+
+    Node* boxTextNode2 = scene_->CreateChild("BoxText2");
+    boxTextNode2->SetPosition(Vector3(-6.0f, 2.0f, 0.0f));
+    Text3D* boxText2 = boxTextNode2->CreateComponent<Text3D>();
+    boxText2->SetText(String("Face Camera Trail (1 Column)"));
+    boxText2->SetFont(cache->GetResource<Font>("Fonts/BlueHighway.sdf"), 24);
+
+    Node* ninjaTextNode2 = scene_->CreateChild("NinjaText");
+    ninjaTextNode2->SetPosition(Vector3(4.0f, 2.5f, 0.0f));
+    Text3D* ninjaText = ninjaTextNode2->CreateComponent<Text3D>();
+    ninjaText->SetText(String("Bone Trail (4 Column)"));
+    ninjaText->SetFont(cache->GetResource<Font>("Fonts/BlueHighway.sdf"), 24);
+
+    // Create the camera.
+    cameraNode_ = scene_->CreateChild("Camera");
+    cameraNode_->CreateComponent<Camera>();
+
+    // Set an initial position for the camera scene node above the plane
+    cameraNode_->SetPosition(Vector3(0.0f, 2.0f, -14.0f));
+}
+
+void RibbonTrailDemo::CreateInstructions()
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    UI* ui = GetSubsystem<UI>();
+
+    // Construct new Text object, set string to display and font to use
+    Text* instructionText = ui->GetRoot()->CreateChild<Text>();
+    instructionText->SetText("Use WASD keys and mouse/touch to move");
+    instructionText->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 15);
+
+    // Position the text relative to the screen center
+    instructionText->SetHorizontalAlignment(HA_CENTER);
+    instructionText->SetVerticalAlignment(VA_CENTER);
+    instructionText->SetPosition(0, ui->GetRoot()->GetHeight() / 4);
+}
+
+void RibbonTrailDemo::SetupViewport()
+{
+    Renderer* renderer = GetSubsystem<Renderer>();
+
+    // Set up a viewport to the Renderer subsystem so that the 3D scene can be seen. We need to define the scene and the camera
+    // at minimum. Additionally we could configure the viewport screen size and the rendering path (eg. forward / deferred) to
+    // use, but now we just use full screen and default render path configured in the engine command line options
+    SharedPtr<Viewport> viewport(new Viewport(context_, scene_, cameraNode_->GetComponent<Camera>()));
+    renderer->SetViewport(0, viewport);
+}
+
+void RibbonTrailDemo::MoveCamera(float timeStep)
+{
+    // Do not move if the UI has a focused element (the console)
+    if (GetSubsystem<UI>()->GetFocusElement())
+        return;
+
+    Input* input = GetSubsystem<Input>();
+
+    // Movement speed as world units per second
+    const float MOVE_SPEED = 20.0f;
+    // Mouse sensitivity as degrees per pixel
+    const float MOUSE_SENSITIVITY = 0.1f;
+
+    // Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
+    IntVector2 mouseMove = input->GetMouseMove();
+    yaw_ += MOUSE_SENSITIVITY * mouseMove.x_;
+    pitch_ += MOUSE_SENSITIVITY * mouseMove.y_;
+    pitch_ = Clamp(pitch_, -90.0f, 90.0f);
+
+    // Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
+    cameraNode_->SetRotation(Quaternion(pitch_, yaw_, 0.0f));
+
+    // Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
+    // Use the Translate() function (default local space) to move relative to the node's orientation.
+    if (input->GetKeyDown(KEY_W))
+        cameraNode_->Translate(Vector3::FORWARD * MOVE_SPEED * timeStep);
+    if (input->GetKeyDown(KEY_S))
+        cameraNode_->Translate(Vector3::BACK * MOVE_SPEED * timeStep);
+    if (input->GetKeyDown(KEY_A))
+        cameraNode_->Translate(Vector3::LEFT * MOVE_SPEED * timeStep);
+    if (input->GetKeyDown(KEY_D))
+        cameraNode_->Translate(Vector3::RIGHT * MOVE_SPEED * timeStep);
+}
+
+void RibbonTrailDemo::SubscribeToEvents()
+{
+    // Subscribe HandleUpdate() function for processing update events
+    SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(RibbonTrailDemo, HandleUpdate));
+}
+
+void RibbonTrailDemo::HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    using namespace Update;
+
+    // Take the frame time step, which is stored as a float
+    float timeStep = eventData[P_TIMESTEP].GetFloat();
+
+    // Move the camera, scale movement with time step
+    MoveCamera(timeStep);
+
+    // Sum of timesteps.
+    timeStepSum_ += timeStep;
+
+    // Move first box with pattern.
+    boxNode1_->SetTransform(
+                Vector3(-4.0 + 3.0 * Cos(100.0 * timeStepSum_), 0.5, -2.0 * Cos(400.0 * timeStepSum_)), Quaternion());
+
+    // Move second box with pattern.
+    boxNode2_->SetTransform(
+                Vector3(0.0 + 3.0 * Cos(100.0 * timeStepSum_), 0.5, -2.0 * Cos(400.0 * timeStepSum_)), Quaternion());
+    
+    // Get elapsed attack animation time.
+    float swordAnimTime = ninjaAnimCtrl_->GetAnimationState(String("Models/NinjaSnowWar/Ninja_Attack3.ani"))->GetTime();
+
+    // Stop emitting trail when sword is finished slashing.
+    if (swordAnimTime > swordTrailStartTime_ && swordAnimTime < swordTrailEndTime_
+            && swordTrail_->IsEmitting() == false)
+        swordTrail_->SetEmitting(true);
+    else if (swordAnimTime >= swordTrailEndTime_ && swordTrail_->IsEmitting() == true)
+        swordTrail_->SetEmitting(false);
+}

+ 82 - 0
Source/Samples/44_RibbonTrailDemo/RibbonTrailDemo.h

@@ -0,0 +1,82 @@
+//
+// Copyright (c) 2008-2016 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 "Sample.h"
+
+namespace Urho3D
+{
+
+class Node;
+class Scene;
+class RibbonTrail;
+
+}
+
+/// Static 3D scene example.
+/// This sample demonstrates:
+///     - Creating a 3D scene with static content
+///     - Displaying the scene using the Renderer subsystem
+///     - Handling keyboard and mouse input to move a freelook camera
+class RibbonTrailDemo : public Sample
+{
+    URHO3D_OBJECT(RibbonTrailDemo, Sample);
+
+public:
+    /// Construct.
+    RibbonTrailDemo(Context* context);
+
+    /// Setup after engine initialization and before running the main loop.
+    virtual void Start();
+
+protected:
+    // Trail that emmited from sword.
+    SharedPtr<RibbonTrail> swordTrail_;
+    // Animation controller of the ninja.
+    SharedPtr<AnimationController> ninjaAnimCtrl_;
+    // The time sword emit trail.
+    float swordTrailStartTime_;
+    // The time sword stop emitting trail.
+    float swordTrailEndTime_;
+    // Box node 1.
+    SharedPtr<Node> boxNode1_;
+    // Box node 2.
+    SharedPtr<Node> boxNode2_;
+    // Sum of timestep.
+    float timeStepSum_;
+
+private:
+    /// Construct the scene content.
+    void CreateScene();
+    /// Construct an instruction text to the UI.
+    void CreateInstructions();
+    /// Set up a viewport for displaying the scene.
+    void SetupViewport();
+    /// Read input and moves the camera.
+    void MoveCamera(float timeStep);
+    /// Subscribe to application-wide logic update events.
+    void SubscribeToEvents();
+    /// Handle the logic update event.
+    void HandleUpdate(StringHash eventType, VariantMap& eventData);
+
+};

+ 40 - 0
Source/Urho3D/AngelScript/GraphicsAPI.cpp

@@ -41,6 +41,7 @@
 #include "../Graphics/ParticleEmitter.h"
 #include "../Graphics/Renderer.h"
 #include "../Graphics/RenderPath.h"
+#include "../Graphics/RibbonTrail.h"
 #include "../Graphics/StaticModelGroup.h"
 #include "../Graphics/Technique.h"
 #include "../Graphics/Terrain.h"
@@ -1609,6 +1610,44 @@ static void RegisterParticleEmitter(asIScriptEngine* engine)
     engine->RegisterObjectMethod("ParticleEmitter", "void ApplyEffect()", asMETHOD(ParticleEmitter, ApplyEffect), asCALL_THISCALL);
 }
 
+static void RegisterRibbonTrail(asIScriptEngine* engine)
+{
+    engine->RegisterEnum("TrailType");
+    engine->RegisterEnumValue("TrailType", "TT_FACE_CAMERA", TT_FACE_CAMERA);
+    engine->RegisterEnumValue("TrailType", "TT_BONE", TT_BONE);
+
+    RegisterDrawable<RibbonTrail>(engine, "RibbonTrail");
+    engine->RegisterObjectMethod("RibbonTrail", "void Commit()", asMETHOD(RibbonTrail, Commit), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_material(Material@+)", asMETHOD(RibbonTrail, SetMaterial), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "Material@+ get_material() const", asMETHOD(RibbonTrail, GetMaterial), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_vertexDistance(float)", asMETHOD(RibbonTrail, SetVertexDistance), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "float get_vertexDistance() const", asMETHOD(RibbonTrail, GetVertexDistance), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_width(float)", asMETHOD(RibbonTrail, SetWidth), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "float get_width() const", asMETHOD(RibbonTrail, GetWidth), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_startColor(const Color&in)", asMETHOD(RibbonTrail, SetStartColor), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "const Color& get_startColor() const", asMETHOD(RibbonTrail, GetStartColor), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_endColor(const Color&in)", asMETHOD(RibbonTrail, SetEndColor), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "const Color& get_endColor() const", asMETHOD(RibbonTrail, GetEndColor), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_startScale(float)", asMETHOD(RibbonTrail, SetStartScale), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "float get_startScale() const", asMETHOD(RibbonTrail, GetStartScale), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_endScale(float)", asMETHOD(RibbonTrail, SetEndScale), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "float get_endScale() const", asMETHOD(RibbonTrail, GetEndScale), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_trailType(TrailType)", asMETHOD(RibbonTrail, SetTrailType), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "TrailType get_trailType() const", asMETHOD(RibbonTrail, GetTrailType), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_sorted(bool)", asMETHOD(RibbonTrail, SetSorted), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "bool get_sorted() const", asMETHOD(RibbonTrail, IsSorted), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_lifetime(float)", asMETHOD(RibbonTrail, SetLifetime), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "float get_lifetime() const", asMETHOD(RibbonTrail, GetLifetime), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_emitting(bool)", asMETHOD(RibbonTrail, SetEmitting), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "bool get_emitting() const", asMETHOD(RibbonTrail, IsEmitting), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_tailColumn(uint)", asMETHOD(RibbonTrail, SetTailColumn), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "uint get_tailColumn() const", asMETHOD(RibbonTrail, GetTailColumn), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "void set_animationLodBias(float)", asMETHOD(RibbonTrail, SetAnimationLodBias), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "float get_animationLodBias() const", asMETHOD(RibbonTrail, GetAnimationLodBias), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RibbonTrail", "Zone@+ get_zone() const", asMETHOD(RibbonTrail, GetZone), asCALL_THISCALL);
+
+}
+
 static void RegisterCustomGeometry(asIScriptEngine* engine)
 {
     engine->RegisterObjectType("CustomGeometryVertex", 0, asOBJ_REF);
@@ -2085,6 +2124,7 @@ void RegisterGraphicsAPI(asIScriptEngine* engine)
     RegisterBillboardSet(engine);
     RegisterParticleEffect(engine);
     RegisterParticleEmitter(engine);
+    RegisterRibbonTrail(engine);
     RegisterCustomGeometry(engine);
     RegisterDecalSet(engine);
     RegisterTerrain(engine);

+ 2 - 0
Source/Urho3D/Graphics/Direct3D11/D3D11Graphics.cpp

@@ -42,6 +42,7 @@
 #include "../../Graphics/Octree.h"
 #include "../../Graphics/ParticleEffect.h"
 #include "../../Graphics/ParticleEmitter.h"
+#include "../../Graphics/RibbonTrail.h"
 #include "../../Graphics/Renderer.h"
 #include "../../Graphics/Shader.h"
 #include "../../Graphics/ShaderPrecache.h"
@@ -2827,6 +2828,7 @@ void RegisterGraphicsLibrary(Context* context)
     BillboardSet::RegisterObject(context);
     ParticleEffect::RegisterObject(context);
     ParticleEmitter::RegisterObject(context);
+    RibbonTrail::RegisterObject(context);
     CustomGeometry::RegisterObject(context);
     DecalSet::RegisterObject(context);
     Terrain::RegisterObject(context);

+ 2 - 0
Source/Urho3D/Graphics/Direct3D9/D3D9Graphics.cpp

@@ -40,6 +40,7 @@
 #include "../../Graphics/Octree.h"
 #include "../../Graphics/ParticleEffect.h"
 #include "../../Graphics/ParticleEmitter.h"
+#include "../../Graphics/RibbonTrail.h"
 #include "../../Graphics/Shader.h"
 #include "../../Graphics/ShaderPrecache.h"
 #include "../../Graphics/ShaderProgram.h"
@@ -2806,6 +2807,7 @@ void RegisterGraphicsLibrary(Context* context)
     BillboardSet::RegisterObject(context);
     ParticleEffect::RegisterObject(context);
     ParticleEmitter::RegisterObject(context);
+    RibbonTrail::RegisterObject(context);
     CustomGeometry::RegisterObject(context);
     DecalSet::RegisterObject(context);
     Terrain::RegisterObject(context);

+ 4 - 2
Source/Urho3D/Graphics/GraphicsDefs.h

@@ -56,8 +56,10 @@ enum GeometryType
     GEOM_INSTANCED = 2,
     GEOM_BILLBOARD = 3,
     GEOM_DIRBILLBOARD = 4,
-    GEOM_STATIC_NOINSTANCING = 5,
-    MAX_GEOMETRYTYPES = 5,
+    GEOM_TRAIL_FACE_CAMERA = 5,
+    GEOM_TRAIL_BONE = 6,
+    GEOM_STATIC_NOINSTANCING = 7,
+    MAX_GEOMETRYTYPES = 7,
 };
 
 /// Blending mode.

+ 2 - 0
Source/Urho3D/Graphics/OpenGL/OGLGraphics.cpp

@@ -43,6 +43,7 @@
 #include "../../Graphics/Octree.h"
 #include "../../Graphics/ParticleEffect.h"
 #include "../../Graphics/ParticleEmitter.h"
+#include "../../Graphics/RibbonTrail.h"
 #include "../../Graphics/RenderSurface.h"
 #include "../../Graphics/Shader.h"
 #include "../../Graphics/ShaderPrecache.h"
@@ -3419,6 +3420,7 @@ void RegisterGraphicsLibrary(Context* context)
     BillboardSet::RegisterObject(context);
     ParticleEffect::RegisterObject(context);
     ParticleEmitter::RegisterObject(context);
+    RibbonTrail::RegisterObject(context);
     CustomGeometry::RegisterObject(context);
     DecalSet::RegisterObject(context);
     Terrain::RegisterObject(context);

+ 3 - 1
Source/Urho3D/Graphics/Renderer.cpp

@@ -181,7 +181,9 @@ static const char* geometryVSVariations[] =
     "SKINNED ",
     "INSTANCED ",
     "BILLBOARD ",
-    "DIRBILLBOARD "
+    "DIRBILLBOARD ",
+    "TRAILFACECAM ",
+    "TRAILBONE "
 };
 
 static const char* lightVSVariations[] =

+ 897 - 0
Source/Urho3D/Graphics/RibbonTrail.cpp

@@ -0,0 +1,897 @@
+//
+// Copyright (c) 2008-2016 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/RibbonTrail.h"
+#include "../Scene/Scene.h"
+#include "../Scene/SceneEvents.h"
+#include "../Resource/ResourceCache.h"
+#include "../Graphics/VertexBuffer.h"
+#include "../Graphics/IndexBuffer.h"
+#include "../Scene/Node.h"
+#include "../Graphics/Camera.h"
+#include "../Graphics/Material.h"
+#include "../Graphics/OctreeQuery.h"
+#include "../Graphics/Geometry.h"
+#include "../IO/Log.h"
+
+namespace Urho3D
+{
+
+extern const char* GEOMETRY_CATEGORY;
+static const unsigned MAX_TAIL_COLUMN = 16;
+
+const char* trailTypeNames[] =
+{
+    "Face Camera",
+    "Bone",
+    0
+};
+
+inline bool CompareTails(Point* lhs, Point* rhs)
+{
+    return lhs->sortDistance_ > rhs->sortDistance_;
+}
+
+RibbonTrail::RibbonTrail(Context* context) : 
+    Drawable(context, DRAWABLE_GEOMETRY),
+    geometry_(new Geometry(context_)),
+    animationLodBias_(1.0f),
+    animationLodTimer_(0.0f),
+    vertexBuffer_(new VertexBuffer(context_)),
+    indexBuffer_(new IndexBuffer(context_)),
+    bufferDirty_(true),
+    previousPosition_(Vector3::ZERO),
+    numPoints_(0),
+    lifetime_(1.0f),
+    vertexDistance_(0.1f),
+    width_(0.2f),
+    startScale_(1.0f),
+    endScale_(1.0f),
+    endColor_(Color(1.0f, 1.0f, 1.0f, 0.0f)),
+    startColor_(Color(1.0f, 1.0f, 1.0f, 1.0f)),
+    lastUpdateFrameNumber_(M_MAX_UNSIGNED),
+    needUpdate_(false),
+    sorted_(false),
+    previousOffset_(Vector3::ZERO),
+    forceUpdate_(false),
+    trailType_(TT_FACE_CAMERA),
+    tailColumn_(1),
+    emitting_(true)
+{
+    geometry_->SetVertexBuffer(0, vertexBuffer_);
+    geometry_->SetIndexBuffer(indexBuffer_);
+
+    transforms_ = Matrix3x4::IDENTITY;
+
+    batches_.Resize(1);
+    batches_[0].geometry_ = geometry_;
+    batches_[0].geometryType_ = GEOM_TRAIL_FACE_CAMERA;
+    batches_[0].worldTransform_ = &transforms_;
+    batches_[0].numWorldTransforms_ = 1;
+
+    // for debug
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    SetMaterial(cache->GetResource<Material>("Materials/TailGenerator.xml"));
+}
+
+RibbonTrail::~RibbonTrail()
+{
+}
+
+void RibbonTrail::RegisterObject(Context* context)
+{
+    context->RegisterFactory<RibbonTrail>(GEOMETRY_CATEGORY);
+
+    URHO3D_ACCESSOR_ATTRIBUTE("Is Enabled", IsEnabled, SetEnabled, bool, true, AM_DEFAULT);
+    URHO3D_COPY_BASE_ATTRIBUTES(Drawable);
+	URHO3D_MIXED_ACCESSOR_ATTRIBUTE("Material", GetMaterialAttr, SetMaterialAttr, ResourceRef, ResourceRef(Material::GetTypeStatic()), AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Emittting", IsEmitting, SetEmitting, bool, true, AM_DEFAULT);
+    URHO3D_ENUM_ACCESSOR_ATTRIBUTE("Trail Type", GetTrailType, SetTrailType, TrailType, trailTypeNames, TT_FACE_CAMERA, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Tail Lifetime", GetLifetime, SetLifetime, float, 1.0f, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Tail Column", GetTailColumn, SetTailColumn, unsigned, 0, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Vertex Distance", GetVertexDistance, SetVertexDistance, float, 0.1f, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Width", GetWidth, SetWidth, float, 0.2f, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Start Scale", GetStartScale, SetStartScale, float, 1.0f, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("End Scale", GetEndScale, SetEndScale, float, 1.0f, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Start Color", GetStartColor, SetStartColor, Color, Color::WHITE, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("End Color", GetEndColor, SetEndColor, Color, Color(1.0f, 1.0f, 1.0f, 0.0f), AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Animation LOD Bias", GetAnimationLodBias, SetAnimationLodBias, float, 1.0f, AM_DEFAULT);
+    URHO3D_ACCESSOR_ATTRIBUTE("Sort By Distance", IsSorted, SetSorted, bool, false, AM_DEFAULT);
+}
+
+void RibbonTrail::ProcessRayQuery(const RayOctreeQuery& query, PODVector<RayQueryResult>& results)
+{
+    // If no billboard-level testing, use the Drawable test
+    if (query.level_ < RAY_TRIANGLE)
+    {
+        Drawable::ProcessRayQuery(query, results);
+        return;
+    }
+
+    // Check ray hit distance to AABB before proceeding with trail-level tests
+    if (query.ray_.HitDistance(GetWorldBoundingBox()) >= query.maxDistance_)
+        return;
+
+    // Approximate the tails as spheres for raycasting
+    for (unsigned i = 0; i < points_.Size() - 1; ++i)
+    {
+        Vector3 center = (points_[i].position_ + points_[i+1].position_) * 0.5f;
+        Vector3 scale = width_ * Vector3::ONE;
+        // Tail should be represented in cylinder shape, but we don't have this yet on Urho,
+        // so this implementation will use bounding box instead (hopefully only temporarily)
+        float distance = query.ray_.HitDistance(BoundingBox(center - scale, center + scale));
+        if (distance < query.maxDistance_)
+        {
+            // If the code reaches here then we have a hit
+            RayQueryResult result;
+            result.position_ = query.ray_.origin_ + distance * query.ray_.direction_;
+            result.normal_ = -query.ray_.direction_;
+            result.distance_ = distance;
+            result.drawable_ = this;
+            result.node_ = node_;
+            result.subObject_ = i;
+            results.Push(result);
+        }
+    }
+}
+
+void RibbonTrail::OnSetEnabled()
+{
+    Drawable::OnSetEnabled();
+
+    previousPosition_ = node_->GetWorldPosition();
+
+    Scene* scene = GetScene();
+    if (scene)
+    {
+        if (IsEnabledEffective())
+            SubscribeToEvent(scene, E_SCENEPOSTUPDATE, URHO3D_HANDLER(RibbonTrail, HandleScenePostUpdate));
+        else
+            UnsubscribeFromEvent(scene, E_SCENEPOSTUPDATE);
+    }
+}
+
+void RibbonTrail::HandleScenePostUpdate(StringHash eventType, VariantMap& eventData)
+{
+    using namespace ScenePostUpdate;
+    lastTimeStep_ = eventData[P_TIMESTEP].GetFloat();
+
+    // Update if frame has changed
+    if (viewFrameNumber_ != lastUpdateFrameNumber_)
+    {
+        // Reset if ribbon trail is too small and too much difference in frame
+        if(points_.Size() < 3 && viewFrameNumber_ - lastUpdateFrameNumber_ > 1)
+        {
+            previousPosition_ = node_->GetWorldPosition();
+            points_.Erase(0, points_.Size());
+        }
+
+        lastUpdateFrameNumber_ = viewFrameNumber_;
+        needUpdate_ = true;
+        MarkForUpdate();
+    }
+}
+
+void RibbonTrail::Update(const FrameInfo &frame)
+{
+    Drawable::Update(frame);
+
+    if (!needUpdate_)
+        return;
+
+    UpdateTail();
+    OnMarkedDirty(node_);
+    needUpdate_ = false;
+}
+
+void RibbonTrail::UpdateTail()
+{
+    Vector3 worldPosition = node_->GetWorldPosition();
+    float path = (previousPosition_ - worldPosition).Length();
+
+    // Update tails lifetime
+    int expiredIndex = -1;
+    if (points_.Size() > 0)
+    {
+        // No need to update last point
+        for (unsigned i = 0; i < points_.Size() - 1; ++i)
+        {
+            points_[i].lifetime_ += lastTimeStep_;
+
+            // Get point index with expired lifetime
+            if(points_[i].lifetime_ > lifetime_)
+                expiredIndex = i;
+        }
+    }
+
+    // Delete expired points
+    if(expiredIndex != -1)
+    {
+        points_.Erase(0, expiredIndex+1);
+
+        // Update endTail pointer
+        if(points_.Size() > 1)
+        {
+            endTail_.position_ = points_[0].position_;
+            startEndTailTime_ = points_[0].lifetime_;
+        }
+    }
+
+    // Update previous world position if trail is still zero
+    if (points_.Size() == 0)
+    {
+        previousPosition_ = worldPosition;
+    }
+    // Delete lonely point
+    else if(points_.Size() == 1)
+    {
+        points_.Erase(0, 1);
+        previousPosition_ = worldPosition;
+    }
+    // Update end of trail position using endTail linear interpolation
+    else if (points_.Size() > 1 && points_[0].lifetime_ < lifetime_)
+    {
+        float step = SmoothStep(startEndTailTime_, lifetime_, points_[0].lifetime_);
+        points_[0].position_ = Lerp(endTail_.position_, points_[1].position_, step);
+        bufferDirty_ = true;
+    }
+
+    // Add starting points
+    if(points_.Size() == 0 && path > M_LARGE_EPSILON && emitting_)
+    {
+        Vector3 forwardmotion = (previousPosition_ - worldPosition).Normalized();
+
+        Point startPoint;
+        startPoint.position_ = previousPosition_;
+        startPoint.lifetime_ = 0.0f;
+        startPoint.forward_ = forwardmotion;
+
+        Point nextPoint;
+        nextPoint.position_ = worldPosition;
+        nextPoint.lifetime_ = 0.0f;
+        nextPoint.forward_ = forwardmotion;
+
+        if(node_->GetParent() != 0)
+        {
+            startPoint.parentPos_ = node_->GetParent()->GetWorldPosition();
+            nextPoint.parentPos_ = node_->GetParent()->GetWorldPosition();
+        }
+
+        points_.Push(startPoint);
+        points_.Push(nextPoint);
+
+        // Update endtail
+        endTail_.position_ = startPoint.position_;
+        startEndTailTime_ = 0.0f;
+    }
+
+    // Add more points
+    if (points_.Size() > 1 && emitting_)
+    {
+        Vector3 forwardmotion = (previousPosition_ - worldPosition).Normalized();
+
+        // Add more point if path exceeded tail length
+        if(path > vertexDistance_)
+        {
+            Point newPoint;
+            newPoint.position_ = worldPosition;
+            newPoint.lifetime_ = 0.0f;
+            newPoint.forward_ = forwardmotion;
+            if(node_->GetParent() != 0)
+                newPoint.parentPos_ = node_->GetParent()->GetWorldPosition();
+
+            points_.Push(newPoint);
+
+            previousPosition_ = worldPosition;
+        }
+        else
+        {
+            // Updating recent tail
+            points_.Back().position_ = worldPosition;
+            if(forwardmotion != Vector3::ZERO)
+                points_.Back().forward_ = forwardmotion;
+        }
+    }
+
+    // Update buffer size if size of points different with tail number
+    if (points_.Size() != numPoints_)
+        bufferSizeDirty_ = true;
+
+}
+
+void RibbonTrail::SetEndScale(float endScale)
+{
+    endScale_ = endScale;
+    Commit();
+}
+
+void RibbonTrail::SetStartScale(float startScale)
+{
+    startScale_ = startScale;
+    Commit();
+}
+
+void RibbonTrail::SetEmitting(bool emitting)
+{
+    if (emitting == emitting_)
+        return;
+
+    emitting_ = emitting;
+
+    // Reset already available points
+    if (emitting == true && points_.Size() > 0)
+    {
+        points_.Clear();
+        bufferSizeDirty_ = true;
+    }
+
+    Drawable::OnMarkedDirty(node_);
+    MarkNetworkUpdate();
+}
+
+void RibbonTrail::SetTailColumn(unsigned tailColumn)
+{
+    if(tailColumn > MAX_TAIL_COLUMN)
+    {
+        URHO3D_LOGINFO("Max tail column is " + String(MAX_TAIL_COLUMN));
+        tailColumn_ = MAX_TAIL_COLUMN;
+    }
+    else if (tailColumn < 1)
+    {
+        tailColumn_ = 1;
+    }
+    else
+        tailColumn_ = tailColumn;
+
+    Drawable::OnMarkedDirty(node_);
+    bufferSizeDirty_ = true;
+    MarkNetworkUpdate();
+}
+
+void RibbonTrail::UpdateBatches(const FrameInfo& frame) 
+{
+    // Update information for renderer about this drawable
+    distance_ = frame.camera_->GetDistance(GetWorldBoundingBox().Center());
+    batches_[0].distance_ = distance_;
+
+    // Calculate scaled distance for animation LOD
+    float scale = GetWorldBoundingBox().Size().DotProduct(DOT_SCALE);
+    // If there are no trail, the size becomes zero, and LOD'ed updates no longer happen. Disable LOD in that case
+    if (scale > M_EPSILON)
+        lodDistance_ = frame.camera_->GetLodDistance(distance_, scale, lodBias_);
+    else
+        lodDistance_ = 0.0f;
+
+    Vector3 worldPos = node_->GetWorldPosition();
+    Vector3 offset = (worldPos - frame.camera_->GetNode()->GetWorldPosition());
+    if (sorted_ && offset != previousOffset_)
+    {
+        bufferDirty_ = true;
+        previousOffset_ = offset;
+    }
+}
+
+void RibbonTrail::UpdateGeometry(const FrameInfo& frame)
+{
+    if (bufferSizeDirty_ || indexBuffer_->IsDataLost())
+        UpdateBufferSize();
+
+    if (bufferDirty_ || vertexBuffer_->IsDataLost())
+        UpdateVertexBuffer(frame);
+}
+
+UpdateGeometryType RibbonTrail::GetUpdateGeometryType()
+{	
+    if (bufferDirty_ || bufferSizeDirty_ || vertexBuffer_->IsDataLost() || indexBuffer_->IsDataLost())
+        return UPDATE_MAIN_THREAD;
+    else
+        return UPDATE_NONE;
+}
+
+void RibbonTrail::SetMaterial(Material* material)
+{
+    batches_[0].material_ = material;
+    MarkNetworkUpdate();
+}
+
+void RibbonTrail::OnSceneSet(Scene* scene)
+{
+    Drawable::OnSceneSet(scene);
+
+    if (scene && IsEnabledEffective())
+        SubscribeToEvent(scene, E_SCENEPOSTUPDATE, URHO3D_HANDLER(RibbonTrail, HandleScenePostUpdate));
+    else if (!scene)
+         UnsubscribeFromEvent(E_SCENEPOSTUPDATE);
+}
+
+void RibbonTrail::OnWorldBoundingBoxUpdate() 
+{
+    BoundingBox worldBox;
+
+    for (unsigned i = 0; i < points_.Size(); ++i)
+    {
+        Vector3 &p = points_[i].position_;
+        Vector3 scale = width_ * Vector3::ONE;
+        worldBox.Merge(BoundingBox(p - scale, p + scale));
+    }
+
+    worldBoundingBox_ = worldBox;
+}
+
+void RibbonTrail::UpdateBufferSize() 
+{
+    numPoints_ = points_.Size();
+
+    unsigned indexPerSegment = 6 + (tailColumn_ - 1) * 6;
+    unsigned vertexPerSegment = 4 + (tailColumn_ - 1) * 2;
+
+    if(trailType_ == TT_FACE_CAMERA)
+    {
+        batches_[0].geometryType_ = GEOM_TRAIL_FACE_CAMERA;
+        vertexBuffer_->SetSize((numPoints_ * vertexPerSegment),
+            MASK_POSITION | MASK_COLOR | MASK_TEXCOORD1 | MASK_TANGENT, true);
+    }
+    else if(trailType_ == TT_BONE)
+    {
+        batches_[0].geometryType_ = GEOM_TRAIL_BONE;
+        vertexBuffer_->SetSize((numPoints_ * vertexPerSegment),
+            MASK_POSITION | MASK_NORMAL | MASK_COLOR | MASK_TEXCOORD1 | MASK_TANGENT, true);
+    }
+
+    if (indexBuffer_->GetIndexCount() != ((numPoints_ - 1) * indexPerSegment))
+        indexBuffer_->SetSize(((numPoints_ - 1) * indexPerSegment), false);
+
+    bufferSizeDirty_ = false;
+    bufferDirty_ = true;
+    forceUpdate_ = true;
+
+    if (numPoints_ == 0)
+        return;
+
+    // Indices do not change for a given tail generator capacity
+    unsigned short* dest = (unsigned short*)indexBuffer_->Lock(0, ((numPoints_ - 1) * indexPerSegment), true);
+    if (!dest)
+        return;
+
+    unsigned vertexIndex = 0;
+    unsigned stripsLen = numPoints_ - 1;
+
+    while (stripsLen--)
+    {
+        dest[0] = (unsigned short)vertexIndex;
+        dest[1] = (unsigned short)(vertexIndex + 2);
+        dest[2] = (unsigned short)(vertexIndex + 1);
+
+        dest[3] = (unsigned short)(vertexIndex + 1);
+        dest[4] = (unsigned short)(vertexIndex + 2);
+        dest[5] = (unsigned short)(vertexIndex + 3);
+
+        dest += 6;
+        //vertexIndex += 4;
+        vertexIndex += 2;
+
+        for (unsigned i = 0; i < (tailColumn_ - 1); ++i)
+        {
+            dest[0] = (unsigned short)vertexIndex;
+            dest[1] = (unsigned short)(vertexIndex + 2);
+            dest[2] = (unsigned short)(vertexIndex + 1);
+
+            dest[3] = (unsigned short)(vertexIndex + 1);
+            dest[4] = (unsigned short)(vertexIndex + 2);
+            dest[5] = (unsigned short)(vertexIndex + 3);
+
+            dest += 6;
+            vertexIndex += 2;
+        }
+
+       vertexIndex += 2;
+
+    }
+
+    indexBuffer_->Unlock();
+    indexBuffer_->ClearDataLost();
+}
+
+void RibbonTrail::UpdateVertexBuffer(const FrameInfo& frame) 
+{
+    // If using animation LOD, accumulate time and see if it is time to update
+    if (animationLodBias_ > 0.0f && lodDistance_ > 0.0f)
+    {
+        animationLodTimer_ += animationLodBias_ * frame.timeStep_ * ANIMATION_LOD_BASESCALE;
+        if (animationLodTimer_ >= lodDistance_)
+            animationLodTimer_ = fmodf(animationLodTimer_, lodDistance_);
+        else
+        {
+            // No LOD if immediate update forced
+            if (!forceUpdate_)
+                return;
+        }
+    }
+
+    // if tail path is short and nothing to draw, exit
+    if (numPoints_ < 2)
+    {
+        batches_[0].geometry_->SetDrawRange(TRIANGLE_LIST, 0, 0, false);
+        return;
+    }
+
+    unsigned indexPerSegment = 6 + (tailColumn_ - 1) * 6;
+    unsigned vertexPerSegment = 4 + (tailColumn_ - 1) * 2;
+
+    // Fill sorted points vector
+    sortedPoints_.Resize(numPoints_);
+    for (unsigned i = 0; i < numPoints_; ++i)
+    {
+        Point& point = points_[i];
+        sortedPoints_[i] = &point;
+        if (sorted_)
+            point.sortDistance_ = frame.camera_->GetDistanceSquared(point.position_);
+    }
+
+    // Sort points
+    if (sorted_)
+        Sort(sortedPoints_.Begin(), sortedPoints_.End(), CompareTails);
+
+    // Update individual trail elapsed length
+    float trailLength = 0.0f;
+    for(unsigned i = 0; i < numPoints_; ++i)
+    {
+        float length = i == 0 ? 0.0f : (points_[i].position_ - points_[i-1].position_).Length();
+        trailLength += length;
+        points_[i].elapsedLength_ = trailLength;
+        if(i < numPoints_ - 1)
+            points_[i].next_ = &points_[i+1];
+    }
+
+    batches_[0].geometry_->SetDrawRange(TRIANGLE_LIST, 0, (numPoints_ - 1) * indexPerSegment, false);
+    bufferDirty_ = false;
+    forceUpdate_ = false;
+
+    float* dest = (float*)vertexBuffer_->Lock(0, (numPoints_ - 1) * vertexPerSegment, true);
+    if (!dest)
+        return;
+
+    // Generate trail mesh
+    if (trailType_ == TT_FACE_CAMERA)
+    {
+        for (unsigned i = 0; i < numPoints_; ++i)
+        {
+            Point& point = *sortedPoints_[i];
+
+            if (sortedPoints_[i] == &points_.Back()) continue;
+
+            // This point
+            float factor = SmoothStep(0.0f, trailLength, point.elapsedLength_);
+            unsigned c = endColor_.Lerp(startColor_, factor).ToUInt();
+            float width = Lerp(width_ * endScale_, width_ * startScale_, factor);
+
+            // Next point
+            float nextFactor = SmoothStep(0.0f, trailLength, point.next_->elapsedLength_);
+            unsigned nextC = endColor_.Lerp(startColor_, nextFactor).ToUInt();
+            float nextWidth = Lerp(width_ * endScale_, width_ * startScale_, nextFactor);
+
+            // First row
+            dest[0] = point.position_.x_;
+            dest[1] = point.position_.y_;
+            dest[2] = point.position_.z_;
+            ((unsigned&)dest[3]) = c;
+            dest[4] = factor;
+            dest[5] = 0.0f;
+            dest[6] = point.forward_.x_;
+            dest[7] = point.forward_.y_;
+            dest[8] = point.forward_.z_;
+            dest[9] = width;
+
+            dest[10] = point.next_->position_.x_;
+            dest[11] = point.next_->position_.y_;
+            dest[12] = point.next_->position_.z_;
+            ((unsigned&)dest[13]) = nextC;
+            dest[14] = nextFactor;
+            dest[15] = 0.0f;
+            dest[16] = point.next_->forward_.x_;
+            dest[17] = point.next_->forward_.y_;
+            dest[18] = point.next_->forward_.z_;
+            dest[19] = nextWidth;
+
+            dest += 20;
+
+            // Middle rows
+            for (unsigned j = 0; j < (tailColumn_ - 1); ++j)
+            {
+                float elapsed = 1.0f / tailColumn_ * (j + 1);
+                float scale = width_ - elapsed * 2.0f * width_;
+                float midWidth = width - elapsed * 2.0f * width;
+                float nextMidWidth = nextWidth - elapsed * 2.0f * nextWidth;
+
+                dest[0] = point.position_.x_;
+                dest[1] = point.position_.y_;
+                dest[2] = point.position_.z_;
+                ((unsigned&)dest[3]) = c;
+                dest[4] = factor;
+                dest[5] = elapsed;
+                dest[6] = point.forward_.x_;
+                dest[7] = point.forward_.y_;
+                dest[8] = point.forward_.z_;
+                dest[9] = midWidth;
+
+                dest[10] = point.next_->position_.x_;
+                dest[11] = point.next_->position_.y_;
+                dest[12] = point.next_->position_.z_;
+                ((unsigned&)dest[13]) = nextC;
+                dest[14] = nextFactor;
+                dest[15] = elapsed;
+                dest[16] = point.next_->forward_.x_;
+                dest[17] = point.next_->forward_.y_;
+                dest[18] = point.next_->forward_.z_;
+                dest[19] = nextMidWidth;
+
+                dest += 20;
+            }
+
+            // Last row
+            dest[0] = point.position_.x_;
+            dest[1] = point.position_.y_;
+            dest[2] = point.position_.z_;
+            ((unsigned&)dest[3]) = c;
+            dest[4] = factor;
+            dest[5] = 1.0f;
+            dest[6] = point.forward_.x_;
+            dest[7] = point.forward_.y_;
+            dest[8] = point.forward_.z_;
+            dest[9] = -width;
+
+            dest[10] = point.next_->position_.x_;
+            dest[11] = point.next_->position_.y_;
+            dest[12] = point.next_->position_.z_;
+            ((unsigned&)dest[13]) = nextC;
+            dest[14] = nextFactor;
+            dest[15] = 1.0f;
+            dest[16] = point.next_->forward_.x_;
+            dest[17] = point.next_->forward_.y_;
+            dest[18] = point.next_->forward_.z_;
+            dest[19] = -nextWidth;
+
+            dest += 20;
+        }
+    }
+    else if(trailType_ == TT_BONE)
+    {
+        for (unsigned i = 0; i < numPoints_; ++i)
+        {
+            Point& point = *sortedPoints_[i];
+
+            if (sortedPoints_[i] == &points_.Back()) continue;
+
+            // This point
+            float factor = SmoothStep(0.0f, trailLength, point.elapsedLength_);
+            unsigned c = endColor_.Lerp(startColor_, factor).ToUInt();
+            //float scale = factor
+
+            float rightScale = Lerp(endScale_, startScale_, factor);
+            float shift = (rightScale - 1.0f) / 2.0f;
+            float leftScale = 0.0f - shift;
+
+            // Next point
+            float nextFactor = SmoothStep(0.0f, trailLength, point.next_->elapsedLength_);
+            unsigned nextC = endColor_.Lerp(startColor_, nextFactor).ToUInt();
+
+            float nextRightScale = Lerp(endScale_, startScale_, nextFactor);
+            float nextShift = (nextRightScale - 1.0f) / 2.0f;
+            float nextLeftScale = 0.0f - nextShift;
+
+            // First row
+            dest[0] = point.position_.x_;
+            dest[1] = point.position_.y_;
+            dest[2] = point.position_.z_;
+            dest[3] = point.forward_.x_;
+            dest[4] = point.forward_.y_;
+            dest[5] = point.forward_.z_;
+            ((unsigned&)dest[6]) = c;
+            dest[7] = factor;
+            dest[8] = 0.0f;
+            dest[9] = point.parentPos_.x_;
+            dest[10] = point.parentPos_.y_;
+            dest[11] = point.parentPos_.z_;
+            dest[12] = leftScale;
+
+            dest[13] = point.next_->position_.x_;
+            dest[14] = point.next_->position_.y_;
+            dest[15] = point.next_->position_.z_;
+            dest[16] = point.next_->forward_.x_;
+            dest[17] = point.next_->forward_.y_;
+            dest[18] = point.next_->forward_.z_;
+            ((unsigned&)dest[19]) = nextC;
+            dest[20] = nextFactor;
+            dest[21] = 0.0f;
+            dest[22] = point.next_->parentPos_.x_;
+            dest[23] = point.next_->parentPos_.y_;
+            dest[24] = point.next_->parentPos_.z_;
+            dest[25] = nextLeftScale;
+
+            dest += 26;
+
+            // Middle row
+            for (unsigned j = 0; j < (tailColumn_ - 1); ++j)
+            {
+                float elapsed = 1.0f / tailColumn_ * (j + 1);
+
+                dest[0] = point.position_.x_;
+                dest[1] = point.position_.y_;
+                dest[2] = point.position_.z_;
+                dest[3] = point.forward_.x_;
+                dest[4] = point.forward_.y_;
+                dest[5] = point.forward_.z_;
+                ((unsigned&)dest[6]) = c;
+                dest[7] = factor;
+                dest[8] = elapsed;
+                dest[9] = point.parentPos_.x_;
+                dest[10] = point.parentPos_.y_;
+                dest[11] = point.parentPos_.z_;
+                dest[12] = Lerp(leftScale, rightScale, elapsed);
+
+                dest[13] = point.next_->position_.x_;
+                dest[14] = point.next_->position_.y_;
+                dest[15] = point.next_->position_.z_;
+                dest[16] = point.next_->forward_.x_;
+                dest[17] = point.next_->forward_.y_;
+                dest[18] = point.next_->forward_.z_;
+                ((unsigned&)dest[19]) = nextC;
+                dest[20] = nextFactor;
+                dest[21] = elapsed;
+                dest[22] = point.next_->parentPos_.x_;
+                dest[23] = point.next_->parentPos_.y_;
+                dest[24] = point.next_->parentPos_.z_;
+                dest[25] = Lerp(nextLeftScale, nextRightScale, elapsed);
+
+                dest += 26;
+            }
+
+            // Last row
+            dest[0] = point.position_.x_;
+            dest[1] = point.position_.y_;
+            dest[2] = point.position_.z_;
+            dest[3] = point.forward_.x_;
+            dest[4] = point.forward_.y_;
+            dest[5] = point.forward_.z_;
+            ((unsigned&)dest[6]) = c;
+            dest[7] = factor;
+            dest[8] = 1.0f;
+            dest[9] = point.parentPos_.x_;
+            dest[10] = point.parentPos_.y_;
+            dest[11] = point.parentPos_.z_;
+            dest[12] = rightScale;
+
+            dest[13] = point.next_->position_.x_;
+            dest[14] = point.next_->position_.y_;
+            dest[15] = point.next_->position_.z_;
+            dest[16] = point.next_->forward_.x_;
+            dest[17] = point.next_->forward_.y_;
+            dest[18] = point.next_->forward_.z_;
+            ((unsigned&)dest[19]) = nextC;
+            dest[20] = nextFactor;
+            dest[21] = 1.0f;
+            dest[22] = point.next_->parentPos_.x_;
+            dest[23] = point.next_->parentPos_.y_;
+            dest[24] = point.next_->parentPos_.z_;
+            dest[25] = nextRightScale;
+
+            dest += 26;
+        }
+    }
+
+    vertexBuffer_->Unlock();
+    vertexBuffer_->ClearDataLost();
+}
+
+void RibbonTrail::SetLifetime(float time)
+{
+    lifetime_ = time;
+    Commit();
+}
+
+void RibbonTrail::SetVertexDistance(float length)
+{
+    vertexDistance_ = length;
+    Commit();
+}
+
+void RibbonTrail::SetEndColor(const Color& c)
+{
+    endColor_ = Color(c.r_, c.g_, c.b_, c.a_);
+    Commit();
+}
+
+void RibbonTrail::SetStartColor(const Color& c)
+{
+    startColor_ = Color(c.r_, c.g_, c.b_, c.a_);
+    Commit();
+}
+
+void RibbonTrail::SetSorted(bool enable)
+{
+    sorted_ = enable;
+    Commit();
+}
+
+void RibbonTrail::SetTrailType(TrailType type)
+{
+    if (trailType_ == type)
+        return;
+
+    if (type == TT_BONE && (node_->GetParent() == 0 || node_->GetParent() == node_->GetScene()))
+    {
+        URHO3D_LOGWARNING("No parent node found, revert back to Face Camera type");
+        return;
+    }
+
+    trailType_ = type;
+    //Commit();
+    Drawable::OnMarkedDirty(node_);
+    bufferSizeDirty_ = true;
+    MarkNetworkUpdate();
+}
+
+void RibbonTrail::SetMaterialAttr(const ResourceRef& value)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    SetMaterial(cache->GetResource<Material>(value.name_));
+    Commit();
+}
+
+void RibbonTrail::SetWidth(float width)
+{
+    width_ = width;
+    Commit();
+}
+
+void RibbonTrail::SetAnimationLodBias(float bias)
+{
+    animationLodBias_ = Max(bias, 0.0f);
+    MarkNetworkUpdate();
+}
+
+void RibbonTrail::Commit()
+{
+    MarkPositionsDirty();
+    MarkNetworkUpdate();
+}
+
+void RibbonTrail::MarkPositionsDirty()
+{
+    Drawable::OnMarkedDirty(node_);
+    bufferDirty_ = true;
+}
+
+Material* RibbonTrail::GetMaterial() const
+{
+    return batches_[0].material_;
+}
+
+ResourceRef RibbonTrail::GetMaterialAttr() const
+{
+    return GetResourceRef(batches_[0].material_, Material::GetTypeStatic());
+}
+
+}

+ 242 - 0
Source/Urho3D/Graphics/RibbonTrail.h

@@ -0,0 +1,242 @@
+//
+// Copyright (c) 2008-2016 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"
+
+namespace Urho3D
+{
+
+enum TrailType
+{
+    TT_FACE_CAMERA = 0,
+    TT_BONE
+};
+
+class IndexBuffer;
+class VertexBuffer;
+
+/// Trail is consisting of series of tails. Two connected points make a tail.
+//struct URHO3D_API Point
+struct Point
+{
+    /// Position.
+    Vector3 position_;
+    /// Forward vector.
+    Vector3 forward_;
+    /// Parent position. Trail bone type uses this.
+    Vector3 parentPos_;
+    /// Elapsed length inside the trail.
+    float elapsedLength_;
+    /// Next point to make a tail.
+    Point* next_;
+    /// Tail time to live.
+    float lifetime_;
+    /// Distance for sorting.
+    float sortDistance_;
+};
+
+//static const unsigned MAX_TAILS = 65536 / 6;
+
+/// Custom component that creates a tail
+class URHO3D_API RibbonTrail : public Drawable
+{
+	URHO3D_OBJECT(RibbonTrail, Drawable);
+
+public:
+    /// Construct.
+    RibbonTrail(Context* context);
+    /// Destruct.
+    virtual ~RibbonTrail();
+    /// Register object factory.
+    static void RegisterObject(Context* context);
+    /// Process octree raycast. May be called from a worker thread.
+    virtual void ProcessRayQuery(const RayOctreeQuery& query, PODVector<RayQueryResult>& results);
+    /// Handle enabled/disabled state change.
+    virtual void OnSetEnabled();
+    /// Update before octree reinsertion. Is called from a main thread.
+    virtual void Update(const FrameInfo &frame);
+    /// 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 material.
+    void SetMaterial(Material* material);
+    /// Set material attribute.
+    void SetMaterialAttr(const ResourceRef& value);
+    /// Set distance between points.
+    void SetVertexDistance(float length);
+    /// Set width of the tail. Only works for face camera trail type.
+    void SetWidth(float width);
+    /// Set vertex blended color for start of trail.
+    void SetStartColor(const Color& c);
+    /// Set vertex blended scale for end of trail.
+    void SetEndColor(const Color& c);
+    /// Set vertex blended color for start of trail.
+    void SetStartScale(float startScale);
+    /// Set vertex blended scale for end of trail.
+    void SetEndScale(float endScale);
+    /// Set how the trail behave.
+    void SetTrailType(TrailType type);
+    /// Set whether tails are sorted by distance. Default false.
+    void SetSorted(bool enable);
+    /// Set tail time to live.
+    void SetLifetime(float time);
+    /// Set whether trail should be emitting.
+    void SetEmitting(bool emitting);
+    /// Set number of column for every tails. Can be useful for fixing distortion at high angle.
+    void SetTailColumn(unsigned tailColumn);
+    /// Set animation LOD bias.
+    void SetAnimationLodBias(float bias);
+    /// Mark for bounding box and vertex buffer update. Call after modifying the trails.
+    void Commit();
+
+    /// Return material.
+    Material* GetMaterial() const;
+
+    /// Return material attribute.
+    ResourceRef GetMaterialAttr() const;
+
+    /// Get distance between points.
+    float GetVertexDistance() const { return vertexDistance_;  }
+
+    /// Get width of the trail.
+    float GetWidth() const { return width_; }
+
+    /// Get vertex blended color for start of trail.
+    const Color& GetStartColor() const { return startColor_; }
+
+    /// Get vertex blended color for end of trail.
+    const Color& GetEndColor() const { return endColor_;  }
+
+    /// Get vertex blended scale for start of trail.
+    float GetStartScale() const { return startScale_; }
+
+    /// Get vertex blended scale for end of trail.
+    float GetEndScale() const { return endScale_; }
+
+    /// Return whether tails are sorted.
+    bool IsSorted() const { return sorted_; }
+
+    /// Return tail time to live.
+    float GetLifetime() const {return lifetime_;}
+
+    /// Return animation LOD bias.
+    float GetAnimationLodBias() const { return animationLodBias_; }
+
+    /// Return how the trail behave.
+    TrailType GetTrailType() const { return trailType_; }
+
+    /// Get number of column for tails.
+    unsigned GetTailColumn() const { return tailColumn_; }
+
+    /// Return whether is currently emitting.
+    bool IsEmitting() const { return emitting_ ; }
+
+protected:
+    /// Handle node being assigned.
+    virtual void OnSceneSet(Scene* scene);
+    /// Recalculate the world-space bounding box.
+    virtual void OnWorldBoundingBoxUpdate();
+    /// Mark vertex buffer to need an update.
+    void MarkPositionsDirty();
+    /// Tails.
+    PODVector<Point> points_;
+    /// Tails sorted flag.
+    bool sorted_;
+    /// Animation LOD bias.
+    float animationLodBias_;
+    /// Animation LOD timer.
+    float animationLodTimer_;
+    /// Trail type.
+    TrailType trailType_;
+
+private:
+    /// Handle scene post-update event.
+    void HandleScenePostUpdate(StringHash eventType, VariantMap& eventData);
+
+    /// Resize RibbonTrail vertex and index buffers.
+    void UpdateBufferSize();
+    /// Rewrite RibbonTrail vertex buffer.
+    void UpdateVertexBuffer(const FrameInfo& frame);
+	/// Update/Rebuild tail mesh only if position changed (called by UpdateBatches())
+	void UpdateTail();
+    /// Geometry.
+    SharedPtr<Geometry> geometry_;
+    /// Vertex buffer.
+    SharedPtr<VertexBuffer> vertexBuffer_;
+    /// Index buffer.
+    SharedPtr<IndexBuffer> indexBuffer_;
+    /// Transform matrices for position and orientation.
+    Matrix3x4 transforms_;
+    /// Buffers need resize flag.
+    bool bufferSizeDirty_;
+    /// Vertex buffer needs rewrite flag.
+    bool bufferDirty_;
+    /// Previous position of tail
+    Vector3 previousPosition_;
+    /// Distance between points. Basically is tail length.
+    float vertexDistance_;
+    /// Width of trail.
+    float width_;
+    /// Number of points.
+    unsigned numPoints_;
+    /// Color for start of trails.
+    Color startColor_;
+    /// Color for end of trails.
+    Color endColor_;
+    /// Scale for start of trails.
+    float startScale_;
+    /// End for start of trails.
+    float endScale_;
+    /// Last scene timestep.
+    float lastTimeStep_;
+    // Lifetime
+    float lifetime_;
+    /// Number of columns for every tails.
+    unsigned tailColumn_;
+    /// Rendering framenumber on which was last updated.
+    unsigned lastUpdateFrameNumber_;
+    /// Need update flag.
+    bool needUpdate_;
+    /// Frame number on which was last sorted.
+    unsigned sortFrameNumber_;
+    /// Previous offset to camera for determining whether sorting is necessary.
+    Vector3 previousOffset_;
+    /// Billboard pointers for sorting.
+    Vector<Point*> sortedPoints_;
+    /// Force update flag (ignore animation LOD momentarily.)
+    bool forceUpdate_;
+    /// Currently emitting flag.
+    bool emitting_;
+
+    /// End of trail point for smoother tail dissapearance.
+    Point endTail_;
+    /// The time the tail become end of trail.
+    float startEndTailTime_;
+};
+
+}

+ 54 - 0
Source/Urho3D/LuaScript/pkgs/Graphics/RibbonTrail.pkg

@@ -0,0 +1,54 @@
+$#include "Graphics/RibbonTrail.h"
+
+enum TrailType
+{
+    TT_FACE_CAMERA,
+    TT_BONE
+};
+
+class RibbonTrail : public Drawable
+{
+    void SetMaterial(Material* material);
+    void SetVertexDistance(float length);
+    void SetWidth(float width);
+    void SetStartColor(const Color& c);
+    void SetEndColor(const Color& c);
+    void SetStartScale(float startScale);
+    void SetEndScale(float endScale);
+    void SetTrailType(TrailType type);
+    void SetSorted(bool enable);
+    void SetLifetime(float time);
+    void SetEmitting(bool emitting);
+    void SetTailColumn(unsigned tailColumn);
+    void SetAnimationLodBias(float bias);
+
+    void Commit();
+
+    Material* GetMaterial() const;
+    float GetVertexDistance() const;
+    float GetWidth() const;
+    const Color& GetStartColor() const;
+    const Color& GetEndColor() const;
+    float GetStartScale() const;
+    float GetEndScale() const;
+    TrailType GetTrailType() const;
+    bool IsSorted() const;
+    float GetLifetime() const;
+    unsigned GetTailColumn() const;
+    bool IsEmitting() const;
+    float GetAnimationLodBias() const;
+
+    tolua_property__get_set Material* material;
+    tolua_property__get_set float vertexDistance;
+    tolua_property__get_set float width;
+    tolua_property__get_set Color& startColor;
+    tolua_property__get_set Color& endColor;
+    tolua_property__get_set float startScale;
+    tolua_property__get_set float endScale;
+    tolua_property__get_set TrailType trailType;
+    tolua_property__is_set bool sorted;
+    tolua_property__get_set float lifetime;
+    tolua_property__get_set unsigned tailColumn;
+    tolua_property__is_set bool emitting;
+    tolua_property__get_set float animationLodBias;
+}

+ 1 - 0
Source/Urho3D/LuaScript/pkgs/GraphicsLuaAPI.pkg

@@ -23,6 +23,7 @@ $pfile "Graphics/ParticleEmitter.pkg"
 $pfile "Graphics/Renderer.pkg"
 $pfile "Graphics/RenderPath.pkg"
 $pfile "Graphics/RenderSurface.pkg"
+$pfile "Graphics/RibbonTrail.pkg"
 $pfile "Graphics/Skeleton.pkg"
 $pfile "Graphics/Skybox.pkg"
 $pfile "Graphics/StaticModel.pkg"

+ 37 - 0
bin/CoreData/Shaders/GLSL/Transform.glsl

@@ -114,6 +114,35 @@ vec3 GetBillboardNormal(vec4 iPos, vec3 iDirection, vec3 iCameraPos)
 }
 #endif
 
+#ifdef TRAILFACECAM
+vec3 GetTrailPos(vec4 iPos, vec3 iFront, float iScale, mat4 modelMatrix)
+{
+    vec3 up = normalize(cCameraPos - iPos.xyz);
+    vec3 right = normalize(cross(iFront, up));
+    return (vec4((iPos.xyz + right * iScale), 1.0) * modelMatrix).xyz;
+}
+
+vec3 GetTrailNormal(vec4 iPos)
+{
+    return normalize(cCameraPos - iPos.xyz);
+}
+#endif
+
+#ifdef TRAILBONE
+vec3 GetTrailPos(vec4 iPos, vec3 iParentPos, float iScale, mat4 modelMatrix)
+{
+    vec3 right = iParentPos - iPos.xyz;
+    return (vec4((iPos.xyz + right * iScale), 1.0) * modelMatrix).xyz;
+}
+
+vec3 GetTrailNormal(vec4 iPos, vec3 iParentPos, vec3 iForward)
+{
+    vec3 left = normalize(iPos.xyz - iParentPos);
+    vec3 up = normalize(cross(normalize(iForward), left));
+    return up;
+}
+#endif
+
 #if defined(SKINNED)
     #define iModelMatrix GetSkinMatrix(iBlendWeights, iBlendIndices)
 #elif defined(INSTANCED)
@@ -128,6 +157,10 @@ vec3 GetWorldPos(mat4 modelMatrix)
         return GetBillboardPos(iPos, iTexCoord1, modelMatrix);
     #elif defined(DIRBILLBOARD)
         return GetBillboardPos(iPos, iNormal, iTangent.xyz, modelMatrix);
+    #elif defined(TRAILFACECAM)
+        return GetTrailPos(iPos, iTangent.xyz, iTangent.w, modelMatrix);
+    #elif defined(TRAILBONE)
+        return GetTrailPos(iPos, iTangent.xyz, iTangent.w, modelMatrix);
     #else
         return (iPos * modelMatrix).xyz;
     #endif
@@ -139,6 +172,10 @@ vec3 GetWorldNormal(mat4 modelMatrix)
         return GetBillboardNormal();
     #elif defined(DIRBILLBOARD)
         return GetBillboardNormal(iPos, iNormal, iTangent.xyz);
+    #elif defined(TRAILFACECAM)
+        return GetTrailNormal(iPos);
+    #elif defined(TRAILBONE)
+        return GetTrailNormal(iPos, iTangent.xyz, iNormal);
     #else
         return normalize(iNormal * GetNormalMatrix(modelMatrix));
     #endif

+ 4 - 0
bin/CoreData/Shaders/GLSL/Unlit.glsl

@@ -20,7 +20,11 @@ void VS()
 
     #ifdef VERTEXCOLOR
         vColor = iColor;
+        //#ifdef TRAIL
+        //    vColor = vec4(normalize(cCameraPos), 1.0);
+        //#endif
     #endif
+
 }
 
 void PS()

+ 3 - 1
bin/CoreData/Shaders/HLSL/Basic.hlsl

@@ -19,8 +19,10 @@ void VS(float4 iPos : POSITION,
     #if defined(BILLBOARD) || defined(DIRBILLBOARD)
         float2 iSize : TEXCOORD1,
     #endif
-    #ifdef DIRBILLBOARD
+    #if defined(DIRBILLBOARD) || defined(TRAILBONE)
         float3 iNormal : NORMAL,
+    #endif
+    #if defined(DIRBILLBOARD) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     #ifdef DIFFMAP

+ 2 - 2
bin/CoreData/Shaders/HLSL/LitParticle.hlsl

@@ -5,7 +5,7 @@
 #include "Fog.hlsl"
 
 void VS(float4 iPos : POSITION,
-    #ifndef BILLBOARD
+    #if !defined(BILLBOARD) && !defined(TRAILFACECAM)
         float3 iNormal : NORMAL,
     #endif
     #ifndef NOUV
@@ -24,7 +24,7 @@ void VS(float4 iPos : POSITION,
     #if defined(BILLBOARD) || defined(DIRBILLBOARD)
         float2 iSize : TEXCOORD1,
     #endif
-    #ifdef DIRBILLBOARD
+    #if defined(DIRBILLBOARD) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     out float2 oTexCoord : TEXCOORD0,

+ 2 - 2
bin/CoreData/Shaders/HLSL/LitSolid.hlsl

@@ -6,7 +6,7 @@
 #include "Fog.hlsl"
 
 void VS(float4 iPos : POSITION,
-    #ifndef BILLBOARD
+    #if !defined(BILLBOARD) && !defined(TRAILFACECAM)
         float3 iNormal : NORMAL,
     #endif
     #ifndef NOUV
@@ -18,7 +18,7 @@ void VS(float4 iPos : POSITION,
     #if defined(LIGHTMAP) || defined(AO)
         float2 iTexCoord2 : TEXCOORD1,
     #endif
-    #if defined(NORMALMAP) || defined(DIRBILLBOARD)
+    #if defined(NORMALMAP) || defined(DIRBILLBOARD) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     #ifdef SKINNED

+ 2 - 2
bin/CoreData/Shaders/HLSL/PBRLitSolid.hlsl

@@ -9,7 +9,7 @@
 #include "IBL.hlsl"
 
 void VS(float4 iPos : POSITION,
-    #ifndef BILLBOARD
+    #if !defined(BILLBOARD) && !defined(TRAILFACECAM)
         float3 iNormal : NORMAL,
     #endif
     #ifndef NOUV
@@ -21,7 +21,7 @@ void VS(float4 iPos : POSITION,
     #if defined(LIGHTMAP) || defined(AO)
         float2 iTexCoord2 : TEXCOORD1,
     #endif
-    #if defined(NORMALMAP) || defined(DIRBILLBOARD) || defined(IBL)
+    #if defined(NORMALMAP) || defined(DIRBILLBOARD) || defined(IBL) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     #ifdef SKINNED

+ 1 - 1
bin/CoreData/Shaders/HLSL/TerrainBlend.hlsl

@@ -51,7 +51,7 @@ void VS(float4 iPos : POSITION,
     #if defined(BILLBOARD) || defined(DIRBILLBOARD)
         float2 iSize : TEXCOORD1,
     #endif
-    #ifdef DIRBILLBOARD
+    #if defined(DIRBILLBOARD) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     out float2 oTexCoord : TEXCOORD0,

+ 37 - 0
bin/CoreData/Shaders/HLSL/Transform.hlsl

@@ -75,6 +75,35 @@ float3 GetBillboardNormal(float4 iPos, float3 iDirection, float3 iCameraPos)
 }
 #endif
 
+#ifdef TRAILFACECAM
+float3 GetTrailPos(float4 iPos, float3 iFront, float iScale, float4x3 modelMatrix)
+{
+    float3 up = normalize(cCameraPos - iPos.xyz);
+    float3 left = normalize(cross(iFront, up));
+    return (mul(float4((iPos.xyz + left * iScale), 1.0), modelMatrix)).xyz;
+}
+
+float3 GetTrailNormal(float4 iPos)
+{
+    return normalize(cCameraPos - iPos.xyz);
+}
+#endif
+
+#ifdef TRAILBONE
+float3 GetTrailPos(float4 iPos, float3 iParentPos, float iScale, float4x3 modelMatrix)
+{
+    float3 right = iParentPos - iPos.xyz;
+    return (mul(float4((iPos.xyz + right * iScale), 1.0), modelMatrix)).xyz;
+}
+
+float3 GetTrailNormal(float4 iPos, float3 iParentPos, float3 iForward)
+{
+    float3 left = normalize(iPos.xyz - iParentPos);
+    float3 up = -normalize(cross(normalize(iForward), left));
+    return up;
+}
+#endif
+
 #if defined(SKINNED)
     #define iModelMatrix GetSkinMatrix(iBlendWeights, iBlendIndices);
 #elif defined(INSTANCED)
@@ -87,6 +116,10 @@ float3 GetBillboardNormal(float4 iPos, float3 iDirection, float3 iCameraPos)
     #define GetWorldPos(modelMatrix) GetBillboardPos(iPos, iSize, modelMatrix)
 #elif defined(DIRBILLBOARD)
     #define GetWorldPos(modelMatrix) GetBillboardPos(iPos, iSize, iNormal, iTangent.xyz, modelMatrix)
+#elif defined(TRAILFACECAM)
+    #define GetWorldPos(modelMatrix) GetTrailPos(iPos, iTangent.xyz, iTangent.w, modelMatrix)
+#elif defined(TRAILBONE)
+    #define GetWorldPos(modelMatrix) GetTrailPos(iPos, iTangent.xyz, iTangent.w, modelMatrix)
 #else
     #define GetWorldPos(modelMatrix) mul(iPos, modelMatrix)
 #endif
@@ -95,6 +128,10 @@ float3 GetBillboardNormal(float4 iPos, float3 iDirection, float3 iCameraPos)
     #define GetWorldNormal(modelMatrix) GetBillboardNormal()
 #elif defined(DIRBILLBOARD)
     #define GetWorldNormal(modelMatrix) GetBillboardNormal(iPos, iNormal, iTangent.xyz)
+#elif defined(TRAILFACECAM)
+    #define GetWorldNormal(modelMatrix) GetTrailNormal(iPos)
+#elif defined(TRAILBONE)
+    #define GetWorldNormal(modelMatrix) GetTrailNormal(iPos, iTangent.xyz, iNormal)
 #else
     #define GetWorldNormal(modelMatrix) normalize(mul(iNormal, (float3x3)modelMatrix))
 #endif

+ 3 - 1
bin/CoreData/Shaders/HLSL/Unlit.hlsl

@@ -20,8 +20,10 @@ void VS(float4 iPos : POSITION,
     #if defined(BILLBOARD) || defined(DIRBILLBOARD)
         float2 iSize : TEXCOORD1,
     #endif
-    #ifdef DIRBILLBOARD
+    #if defined(DIRBILLBOARD) || defined(TRAILBONE)
         float3 iNormal : NORMAL,
+    #endif
+    #if defined(DIRBILLBOARD) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     out float2 oTexCoord : TEXCOORD0,

+ 2 - 2
bin/CoreData/Shaders/HLSL/Vegetation.hlsl

@@ -27,7 +27,7 @@ cbuffer CustomVS : register(b6)
 #endif
 
 void VS(float4 iPos : POSITION,
-    #ifndef BILLBOARD
+    #if !defined(BILLBOARD) && !defined(TRAILFACECAM)
         float3 iNormal : NORMAL,
     #endif
     #ifndef NOUV
@@ -39,7 +39,7 @@ void VS(float4 iPos : POSITION,
     #if defined(LIGHTMAP) || defined(AO)
         float2 iTexCoord2 : TEXCOORD1,
     #endif
-    #if defined(NORMALMAP) || defined(DIRBILLBOARD)
+    #if defined(NORMALMAP) || defined(DIRBILLBOARD) || defined(TRAILFACECAM) || defined(TRAILBONE)
         float4 iTangent : TANGENT,
     #endif
     #ifdef SKINNED

+ 222 - 0
bin/Data/LuaScripts/44_RibbonTrailDemo.lua

@@ -0,0 +1,222 @@
+-- Static 3D scene example.
+-- This sample demonstrates:
+--     - Creating a 3D scene with static content
+--     - Displaying the scene using the Renderer subsystem
+--     - Handling keyboard and mouse input to move a freelook camera
+
+require "LuaScripts/Utilities/Sample"
+
+local boxNode1 = nil
+local boxNode2 = nil
+local swordTrail = nil
+local ninjaAnimCtrl = nil
+local timeStepSum = 0.0
+local swordTrailStartTime = 0.2
+local swordTrailEndTime = 0.46
+
+function Start()
+    -- Execute the common startup for samples
+    SampleStart()
+
+    -- Create the scene content
+    CreateScene()
+
+    -- Create the UI content
+    CreateInstructions()
+
+    -- Setup the viewport for displaying the scene
+    SetupViewport()
+
+    -- Set the mouse mode to use in the sample
+    SampleInitMouseMode(MM_RELATIVE)
+
+    -- Hook up to the frame update events
+    SubscribeToEvents()
+end
+
+function CreateScene()
+    scene_ = Scene()
+
+    -- Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000)
+    scene_:CreateComponent("Octree")
+
+    -- Create scene node & StaticModel component for showing a static plane
+    local planeNode = scene_:CreateChild("Plane")
+    planeNode.scale = Vector3(100.0, 1.0, 100.0)
+    local planeObject = planeNode:CreateComponent("StaticModel")
+    planeObject.model = cache:GetResource("Model", "Models/Plane.mdl")
+    planeObject.material = cache:GetResource("Material", "Materials/StoneTiled.xml")
+
+    -- Create a directional light to the world.
+    local lightNode = scene_:CreateChild("DirectionalLight")
+    lightNode.direction = Vector3(0.6, -1.0, 0.8) -- The direction vector does not need to be normalized
+    local light = lightNode:CreateComponent("Light")
+    light.lightType = LIGHT_DIRECTIONAL
+    light.castShadows = true
+    light.shadowBias = BiasParameters(0.00005, 0.5)
+    -- Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
+    light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8)
+
+    -- Create first box for face camera trail demo with 1 column.
+    boxNode1 = scene_:CreateChild("Box1")
+    local box1 = boxNode1:CreateComponent("StaticModel")
+    box1.model = cache:GetResource("Model", "Models/Box.mdl")
+    box1.castShadows = true
+    local boxTrail1 = boxNode1:CreateComponent("RibbonTrail")
+    boxTrail1.material = cache:GetResource("Material", "Materials/RibbonTrail.xml")
+    boxTrail1.startColor = Color(1.0, 0.5, 0.0, 1.0)
+    boxTrail1.endColor = Color(1.0, 1.0, 0.0, 0.0)
+    boxTrail1.width = 0.5
+
+    -- Create second box for face camera trail demo with 4 column.
+    -- This will produce less distortion than first trail.
+    boxNode2 = scene_:CreateChild("Box2")
+    local box2 = boxNode2:CreateComponent("StaticModel")
+    box2.model = cache:GetResource("Model", "Models/Box.mdl")
+    box2.castShadows = true
+    local boxTrail2 = boxNode2:CreateComponent("RibbonTrail")
+    boxTrail2.material = cache:GetResource("Material", "Materials/RibbonTrail.xml")
+    boxTrail2.startColor = Color(1.0, 0.5, 0.0, 1.0)
+    boxTrail2.endColor = Color(1.0, 1.0, 0.0, 0.0)
+    boxTrail2.width = 0.5
+    boxTrail2.tailColumn = 4
+
+    -- Load ninja animated model for bone trail demo.
+    local ninjaNode = scene_:CreateChild("Ninja")
+    ninjaNode.position = Vector3(5.0, 0.0, 0.0)
+    ninjaNode.rotation = Quaternion(0.0, 180.0, 0.0)
+    local ninja = ninjaNode:CreateComponent("AnimatedModel")
+    ninja.model = cache:GetResource("Model", "Models/NinjaSnowWar/Ninja.mdl")
+    ninja.material = cache:GetResource("Material", "Materials/NinjaSnowWar/Ninja.xml")
+    ninja.castShadows = true
+
+    -- Create animation controller and play attack animation.
+    ninjaAnimCtrl = ninjaNode:CreateComponent("AnimationController")
+    ninjaAnimCtrl:PlayExclusive("Models/NinjaSnowWar/Ninja_Attack3.ani", 0, true, 0.0)
+
+    -- Add ribbon trail to tip of sword.
+    local swordTip = ninjaNode:GetChild("Joint29", true)
+    swordTrail = swordTip:CreateComponent("RibbonTrail")
+
+    -- Set sword trail type to bone and set other parameters.
+    swordTrail.trailType = TT_BONE
+    swordTrail.material = cache:GetResource("Material", "Materials/SlashTrail.xml")
+    swordTrail.lifetime = 0.22
+    swordTrail.startColor = Color(1.0, 1.0, 1.0, 0.75)
+    swordTrail.endColor = Color(0.2, 0.5, 1.0, 0.0)
+    swordTrail.tailColumn = 4
+
+    -- Add floating text for info.
+    local boxTextNode1 = scene_:CreateChild("BoxText1")
+    boxTextNode1.position = Vector3(-1.0, 2.0, 0.0)
+    local boxText1 = boxTextNode1:CreateComponent("Text3D")
+    boxText1.text = "Face Camera Trail (4 Column)"
+    boxText1:SetFont(cache:GetResource("Font", "Fonts/BlueHighway.sdf"), 24)
+
+    local boxTextNode2 = scene_:CreateChild("BoxText2")
+    boxTextNode2.position = Vector3(-6.0, 2.0, 0.0)
+    local boxText2 = boxTextNode2:CreateComponent("Text3D")
+    boxText2.text = "Face Camera Trail (1 Column)"
+    boxText2:SetFont(cache:GetResource("Font", "Fonts/BlueHighway.sdf"), 24)
+
+    local ninjaTextNode2 = scene_:CreateChild("NinjaText")
+    ninjaTextNode2.position = Vector3(4.0, 2.5, 0.0)
+    local ninjaText = ninjaTextNode2:CreateComponent("Text3D")
+    ninjaText.text = "Bone Trail (4 Column)"
+    ninjaText:SetFont(cache:GetResource("Font", "Fonts/BlueHighway.sdf"), 24)
+
+    -- Create the camera.
+    cameraNode = scene_:CreateChild("Camera")
+    cameraNode:CreateComponent("Camera")
+
+    -- Set an initial position for the camera scene node above the plane
+    cameraNode.position = Vector3(0.0, 2.0, -14.0)
+end
+
+function CreateInstructions()
+    -- Construct new Text object, set string to display and font to use
+    local instructionText = ui.root:CreateChild("Text")
+    instructionText:SetText("Use WASD keys and mouse to move")
+    instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
+
+    -- Position the text relative to the screen center
+    instructionText.horizontalAlignment = HA_CENTER
+    instructionText.verticalAlignment = VA_CENTER
+    instructionText:SetPosition(0, ui.root.height / 4)
+end
+
+function SetupViewport()
+    -- Set up a viewport to the Renderer subsystem so that the 3D scene can be seen. We need to define the scene and the camera
+    -- at minimum. Additionally we could configure the viewport screen size and the rendering path (eg. forward / deferred) to
+    -- use, but now we just use full screen and default render path configured in the engine command line options
+    local viewport = Viewport:new(scene_, cameraNode:GetComponent("Camera"))
+    renderer:SetViewport(0, viewport)
+end
+
+function MoveCamera(timeStep)
+    -- Do not move if the UI has a focused element (the console)
+    if ui.focusElement ~= nil then
+        return
+    end
+
+    -- Movement speed as world units per second
+    local MOVE_SPEED = 20.0
+    -- Mouse sensitivity as degrees per pixel
+    local MOUSE_SENSITIVITY = 0.1
+
+    -- Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
+    local mouseMove = input.mouseMove
+    yaw = yaw +MOUSE_SENSITIVITY * mouseMove.x
+    pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y
+    pitch = Clamp(pitch, -90.0, 90.0)
+
+    -- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
+
+    -- Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
+    -- Use the Translate() function (default local space) to move relative to the node's orientation.
+    if input:GetKeyDown(KEY_W) then
+        cameraNode:Translate(Vector3(0.0, 0.0, 1.0) * MOVE_SPEED * timeStep)
+    end
+    if input:GetKeyDown(KEY_S) then
+        cameraNode:Translate(Vector3(0.0, 0.0, -1.0) * MOVE_SPEED * timeStep)
+    end
+    if input:GetKeyDown(KEY_A) then
+        cameraNode:Translate(Vector3(-1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
+    end
+    if input:GetKeyDown(KEY_D) then
+        cameraNode:Translate(Vector3(1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
+    end
+end
+
+function SubscribeToEvents()
+    -- Subscribe HandleUpdate() function for processing update events
+    SubscribeToEvent("Update", "HandleUpdate")
+end
+
+function HandleUpdate(eventType, eventData)
+    -- Take the frame time step, which is stored as a float
+    local timeStep = eventData["TimeStep"]:GetFloat()
+
+    -- Move the camera, scale movement with time step
+    MoveCamera(timeStep)
+
+    -- Sum of timesteps.
+    timeStepSum = timeStepSum + timeStep
+    
+    -- Move first box with pattern.
+    boxNode1:SetTransform(Vector3(-4.0 + 3.0 * Cos(100.0 * timeStepSum), 0.5, -2.0 * Cos(400.0 * timeStepSum)), Quaternion())
+    
+    -- Move second box with pattern.
+    boxNode2:SetTransform(Vector3(0.0 + 3.0 * Cos(100.0 * timeStepSum), 0.5, -2.0 * Cos(400.0 * timeStepSum)), Quaternion())
+
+    -- Get elapsed attack animation time.
+    local swordAnimTime = ninjaAnimCtrl:GetAnimationState("Models/NinjaSnowWar/Ninja_Attack3.ani").time
+
+    -- Stop emitting trail when sword is finished slashing.
+    if swordAnimTime > swordTrailStartTime and swordAnimTime < swordTrailEndTime and swordTrail.emitting == false then
+        swordTrail.emitting = true
+    elseif swordAnimTime >= swordTrailEndTime and swordTrail.emitting == true then
+        swordTrail.emitting = false
+    end
+end

+ 14 - 0
bin/Data/Materials/RibbonTrail.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<material>
+    <technique name="Techniques/DiffVColUnlitAlpha.xml" quality="1" loddistance="0" />
+    <texture unit="diffuse" name="Textures/RibbonTrail.png" />
+	<parameter name="UOffset" value="1 0 0 0" />
+	<parameter name="VOffset" value="0 1 0 0" />
+	<parameter name="MatDiffColor" value="1 1 1 1" />
+	<parameter name="MatEmissiveColor" value="0 0 0" />
+	<parameter name="MatEnvMapColor" value="1 1 1" />
+	<parameter name="MatSpecColor" value="0.3 0.3 0.3 16" />
+	<cull value="ccw" />
+	<shadowcull value="ccw" />
+	<depthbias constant="0" slopescaled="0" />
+</material>

+ 14 - 0
bin/Data/Materials/SlashTrail.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<material>
+    <technique name="Techniques/DiffVColUnlitAlpha.xml" quality="1" loddistance="0" />
+    <texture unit="diffuse" name="Textures/SlashTrail.png" />
+	<parameter name="UOffset" value="1 0 0 0" />
+	<parameter name="VOffset" value="0 1 0 0" />
+	<parameter name="MatDiffColor" value="1 1 1 1" />
+	<parameter name="MatEmissiveColor" value="0 0 0" />
+	<parameter name="MatEnvMapColor" value="1 1 1" />
+	<parameter name="MatSpecColor" value="0.3 0.3 0.3 16" />
+	<cull value="none" />
+	<shadowcull value="ccw" />
+	<depthbias constant="0" slopescaled="0" />
+</material>

+ 228 - 0
bin/Data/Scripts/44_RibbonTrailDemo.as

@@ -0,0 +1,228 @@
+// Static 3D scene example.
+// This sample demonstrates:
+//     - Creating a 3D scene with static content
+//     - Displaying the scene using the Renderer subsystem
+//     - Handling keyboard and mouse input to move a freelook camera
+
+#include "Scripts/Utilities/Sample.as"
+
+Node@ boxNode1;
+Node@ boxNode2;
+RibbonTrail@ swordTrail;
+AnimationController@ ninjaAnimCtrl;
+float timeStepSum = 0.0f;
+float swordTrailStartTime = 0.2f;
+float swordTrailEndTime = 0.46f;
+
+void Start()
+{
+    // Execute the common startup for samples
+    SampleStart();
+
+    // Create the scene content
+    CreateScene();
+
+    // Create the UI content
+    CreateInstructions();
+
+    // Setup the viewport for displaying the scene
+    SetupViewport();
+
+    // Set the mouse mode to use in the sample
+    SampleInitMouseMode(MM_RELATIVE);
+
+    // Hook up to the frame update events
+    SubscribeToEvents();
+}
+
+void CreateScene()
+{
+    scene_ = Scene();
+
+    // Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000)
+    scene_.CreateComponent("Octree");
+
+    // Create scene node & StaticModel component for showing a static plane
+    Node@ planeNode = scene_.CreateChild("Plane");
+    planeNode.scale = Vector3(100.0f, 1.0f, 100.0f);
+    StaticModel@ planeObject = planeNode.CreateComponent("StaticModel");
+    planeObject.model = cache.GetResource("Model", "Models/Plane.mdl");
+    planeObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");
+
+    // Create a directional light to the world.
+    Node@ lightNode = scene_.CreateChild("DirectionalLight");
+    lightNode.direction = Vector3(0.6f, -1.0f, 0.8f); // The direction vector does not need to be normalized
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.castShadows = true;
+    light.shadowBias = BiasParameters(0.00005f, 0.5f);
+    // Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
+    light.shadowCascade = CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f);
+    
+    // Create first box for face camera trail demo with 1 column.
+    boxNode1 = scene_.CreateChild("Box1");
+    StaticModel@ box1 = boxNode1.CreateComponent("StaticModel");
+    box1.model = cache.GetResource("Model", "Models/Box.mdl");
+    box1.castShadows = true;
+    RibbonTrail@ boxTrail1 = boxNode1.CreateComponent("RibbonTrail");
+    boxTrail1.material = cache.GetResource("Material", "Materials/RibbonTrail.xml");
+    boxTrail1.startColor = Color(1.0f, 0.5f, 0.0f, 1.0f);
+    boxTrail1.endColor = Color(1.0f, 1.0f, 0.0f, 0.0f);
+    boxTrail1.width = 0.5f;
+    
+    // Create second box for face camera trail demo with 4 column.
+    // This will produce less distortion than first trail.
+    boxNode2 = scene_.CreateChild("Box2");
+    StaticModel@ box2 = boxNode2.CreateComponent("StaticModel");
+    box2.model = cache.GetResource("Model", "Models/Box.mdl");
+    box2.castShadows = true;
+    RibbonTrail@ boxTrail2 = boxNode2.CreateComponent("RibbonTrail");
+    boxTrail2.material = cache.GetResource("Material", "Materials/RibbonTrail.xml");
+    boxTrail2.startColor = Color(1.0f, 0.5f, 0.0f, 1.0f);
+    boxTrail2.endColor = Color(1.0f, 1.0f, 0.0f, 0.0f);
+    boxTrail2.width = 0.5f;
+    boxTrail2.tailColumn = 4;
+    
+    // Load ninja animated model for bone trail demo.
+    Node@ ninjaNode = scene_.CreateChild("Ninja");
+    ninjaNode.position = Vector3(5.0f, 0.0f, 0.0f);
+    ninjaNode.rotation = Quaternion(0.0f, 180.0f, 0.0f);
+    AnimatedModel@ ninja = ninjaNode.CreateComponent("AnimatedModel");
+    ninja.model = cache.GetResource("Model", "Models/NinjaSnowWar/Ninja.mdl");
+    ninja.material = cache.GetResource("Material", "Materials/NinjaSnowWar/Ninja.xml");
+    ninja.castShadows = true;
+
+    // Create animation controller and play attack animation.
+    ninjaAnimCtrl = ninjaNode.CreateComponent("AnimationController");
+    ninjaAnimCtrl.PlayExclusive("Models/NinjaSnowWar/Ninja_Attack3.ani", 0, true, 0.0f);
+
+    // Add ribbon trail to tip of sword.
+    Node@ swordTip = ninjaNode.GetChild("Joint29", true);
+    swordTrail = swordTip.CreateComponent("RibbonTrail");
+
+    // Set sword trail type to bone and set other parameters.
+    swordTrail.trailType = TT_BONE;
+    swordTrail.material = cache.GetResource("Material", "Materials/SlashTrail.xml");
+    swordTrail.lifetime = 0.22f;
+    swordTrail.startColor = Color(1.0f, 1.0f, 1.0f, 0.75f);
+    swordTrail.endColor = Color(0.2, 0.5f, 1.0f, 0.0f);
+    swordTrail.tailColumn = 4;
+    
+    // Add floating text for info.
+    Node@ boxTextNode1 = scene_.CreateChild("BoxText1");
+    boxTextNode1.position = Vector3(-1.0f, 2.0f, 0.0f);
+    Text3D@ boxText1 = boxTextNode1.CreateComponent("Text3D");
+    boxText1.text = "Face Camera Trail (4 Column)";
+    boxText1.SetFont(cache.GetResource("Font", "Fonts/BlueHighway.sdf"), 24);
+
+    Node@ boxTextNode2 = scene_.CreateChild("BoxText2");
+    boxTextNode2.position = Vector3(-6.0f, 2.0f, 0.0f);
+    Text3D@ boxText2 = boxTextNode2.CreateComponent("Text3D");
+    boxText2.text = "Face Camera Trail (1 Column)";
+    boxText2.SetFont(cache.GetResource("Font", "Fonts/BlueHighway.sdf"), 24);
+
+    Node@ ninjaTextNode2 = scene_.CreateChild("NinjaText");
+    ninjaTextNode2.position = Vector3(4.0f, 2.5f, 0.0f);
+    Text3D@ ninjaText = ninjaTextNode2.CreateComponent("Text3D");
+    ninjaText.text = "Bone Trail (4 Column)";
+    ninjaText.SetFont(cache.GetResource("Font", "Fonts/BlueHighway.sdf"), 24);
+
+    // Create the camera.
+    cameraNode = scene_.CreateChild("Camera");
+    cameraNode.CreateComponent("Camera");
+
+    // Set an initial position for the camera scene node above the plane
+    cameraNode.position = Vector3(0.0f, 2.0f, -14.0f);
+}
+
+void CreateInstructions()
+{
+    // Construct new Text object, set string to display and font to use
+    Text@ instructionText = ui.root.CreateChild("Text");
+    instructionText.text = "Use WASD keys and mouse to move";
+    instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
+
+    // Position the text relative to the screen center
+    instructionText.horizontalAlignment = HA_CENTER;
+    instructionText.verticalAlignment = VA_CENTER;
+    instructionText.SetPosition(0, ui.root.height / 4);
+}
+
+void SetupViewport()
+{
+    // Set up a viewport to the Renderer subsystem so that the 3D scene can be seen. We need to define the scene and the camera
+    // at minimum. Additionally we could configure the viewport screen size and the rendering path (eg. forward / deferred) to
+    // use, but now we just use full screen and default render path configured in the engine command line options
+    Viewport@ viewport = Viewport(scene_, cameraNode.GetComponent("Camera"));
+    renderer.viewports[0] = viewport;
+}
+
+void MoveCamera(float timeStep)
+{
+    // Do not move if the UI has a focused element (the console)
+    if (ui.focusElement !is null)
+        return;
+
+    // Movement speed as world units per second
+    const float MOVE_SPEED = 20.0f;
+    // Mouse sensitivity as degrees per pixel
+    const float MOUSE_SENSITIVITY = 0.1f;
+
+    // Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
+    IntVector2 mouseMove = input.mouseMove;
+    yaw += MOUSE_SENSITIVITY * mouseMove.x;
+    pitch += MOUSE_SENSITIVITY * mouseMove.y;
+    pitch = Clamp(pitch, -90.0f, 90.0f);
+
+    // Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
+
+    // Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
+    // Use the Translate() function (default local space) to move relative to the node's orientation.
+    if (input.keyDown[KEY_W])
+        cameraNode.Translate(Vector3(0.0f, 0.0f, 1.0f) * MOVE_SPEED * timeStep);
+    if (input.keyDown[KEY_S])
+        cameraNode.Translate(Vector3(0.0f, 0.0f, -1.0f) * MOVE_SPEED * timeStep);
+    if (input.keyDown[KEY_A])
+        cameraNode.Translate(Vector3(-1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
+    if (input.keyDown[KEY_D])
+        cameraNode.Translate(Vector3(1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
+}
+
+void SubscribeToEvents()
+{
+    // Subscribe HandleUpdate() function for processing update events
+    SubscribeToEvent("Update", "HandleUpdate");
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    // Take the frame time step, which is stored as a float
+    float timeStep = eventData["TimeStep"].GetFloat();
+
+    // Move the camera, scale movement with time step
+    MoveCamera(timeStep);
+    
+    // Sum of timesteps.
+    timeStepSum += timeStep;
+    
+    // Move first box with pattern.
+    boxNode1.SetTransform(
+                Vector3(-4.0 + 3.0 * Cos(100.0 * timeStepSum), 0.5, -2.0 * Cos(400.0 * timeStepSum)), Quaternion());
+    
+    // Move second box with pattern.
+    boxNode2.SetTransform(
+                Vector3(0.0 + 3.0 * Cos(100.0 * timeStepSum), 0.5, -2.0 * Cos(400.0 * timeStepSum)), Quaternion());
+    
+    // Get elapsed attack animation time.
+    float swordAnimTime = ninjaAnimCtrl.GetAnimationState("Models/NinjaSnowWar/Ninja_Attack3.ani").time;
+
+    // Stop emitting trail when sword is finished slashing.
+    if (swordAnimTime > swordTrailStartTime && swordAnimTime < swordTrailEndTime && swordTrail.emitting == false)
+        swordTrail.emitting = true;
+    else if (swordAnimTime >= swordTrailEndTime && swordTrail.emitting == true)
+        swordTrail.emitting = false;
+}
+
+// Create XML patch instructions for screen joystick layout specific to this sample app
+String patchInstructions = "";

+ 5 - 1
bin/Data/Scripts/Editor/EditorSecondaryToolbar.as

@@ -52,6 +52,10 @@ void CreateSecondaryToolBar()
     secondaryToolBar.AddChild(b);
     SubscribeToEvent(b, "Released", "SmallToolBarCreateComponent");
 
+    b = CreateSmallToolBarButton("RibbonTrail");
+    secondaryToolBar.AddChild(b);
+    SubscribeToEvent(b, "Released", "SmallToolBarCreateComponent");
+
     b = CreateSmallToolBarButton("Skybox");
     secondaryToolBar.AddChild(b);
     SubscribeToEvent(b, "Released", "SmallToolBarCreateComponent");
@@ -168,4 +172,4 @@ void SmallToolBarCreateComponent(StringHash eventType, VariantMap& eventData)
 {
     Button@ b = GetEventSender();
     CreateComponent(b.name);
-}
+}

+ 0 - 0
bin/Data/Textures/RibbonTrail.png


+ 0 - 0
bin/Data/Textures/SlashTrail.png


+ 4 - 0
bin/Data/UI/EditorIcons.xml

@@ -427,4 +427,8 @@
         <attribute name="Texture" value="Texture2D;Textures/Editor/EditorIcons.png" />
         <attribute name="Image Rect" value="112 16 126 30" />
     </element>
+    <element type="RibbonTrail">
+        <attribute name="Texture" value="Texture2D;Textures/Editor/EditorIcons.png" />
+        <attribute name="Image Rect" value="176 16 190 30" />
+    </element>
 </elements>