diff --git a/src/base/math.h b/src/base/math.h index edbee831d..1a6692ea0 100644 --- a/src/base/math.h +++ b/src/base/math.h @@ -27,6 +27,20 @@ constexpr inline T mix(const T a, const T b, TB amount) return a + (b - a) * amount; } +template +inline T bezier(const T p0, const T p1, const T p2, const T p3, TB amount) +{ + // De-Casteljau Algorithm + const T c10 = mix(p0, p1, amount); + const T c11 = mix(p1, p2, amount); + const T c12 = mix(p2, p3, amount); + + const T c20 = mix(c10, c11, amount); + const T c21 = mix(c11, c12, amount); + + return mix(c20, c21, amount); // c30 +} + inline float random_float() { return rand() / (float)(RAND_MAX); diff --git a/src/engine/shared/map.h b/src/engine/shared/map.h index 966c639f8..b5395f08b 100644 --- a/src/engine/shared/map.h +++ b/src/engine/shared/map.h @@ -15,6 +15,8 @@ class CMap : public IEngineMap public: CMap(); + CDataFileReader *GetReader() { return &m_DataFile; } + void *GetData(int Index) override; int GetDataSize(int Index) const override; void *GetDataSwapped(int Index) override; diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp index 043f96c89..27519dcb0 100644 --- a/src/game/client/components/maplayers.cpp +++ b/src/game/client/components/maplayers.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -60,22 +61,15 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels CMapLayers *pThis = (CMapLayers *)pUser; Channels = ColorRGBA(); - CEnvPoint *pPoints = 0; + const CMapBasedEnvelopePointAccess EnvelopePoints(pThis->m_pLayers->Map()); - { - int Start, Num; - pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVPOINTS, &Start, &Num); - if(Num) - pPoints = (CEnvPoint *)pThis->m_pLayers->Map()->GetItem(Start); - } + int EnvStart, EnvNum; + pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); - int Start, Num; - pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVELOPE, &Start, &Num); - - if(Env >= Num) + if(EnvelopePoints.NumPoints() == 0 || Env < 0 || Env >= EnvNum) return; - CMapItemEnvelope *pItem = (CMapItemEnvelope *)pThis->m_pLayers->Map()->GetItem(Start + Env); + const CMapItemEnvelope *pItem = (CMapItemEnvelope *)pThis->m_pLayers->Map()->GetItem(EnvStart + Env); const auto TickToNanoSeconds = std::chrono::nanoseconds(1s) / (int64_t)pThis->Client()->GameTickSpeed(); @@ -117,7 +111,7 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels MinTick * TickToNanoSeconds; } } - CRenderTools::RenderEvalEnvelope(pPoints + pItem->m_StartPoint, pItem->m_NumPoints, 4, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Channels); + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, 4, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Channels); } else { @@ -142,7 +136,7 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels s_Time += CurTime - s_LastLocalTime; s_LastLocalTime = CurTime; } - CRenderTools::RenderEvalEnvelope(pPoints + pItem->m_StartPoint, pItem->m_NumPoints, 4, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Channels); + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, 4, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Channels); } } diff --git a/src/game/client/render.h b/src/game/client/render.h index 1ac873dcd..6af7607be 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -20,6 +20,8 @@ struct CDataSprite; } struct CDataSprite; struct CEnvPoint; +struct CEnvPointBezier; +struct CEnvPointBezier_upstream; struct CMapItemGroup; struct CMapItemGroupEx; struct CQuad; @@ -71,6 +73,29 @@ enum TILERENDERFLAG_EXTEND = 4, }; +class IEnvelopePointAccess +{ +public: + virtual int NumPoints() const = 0; + virtual const CEnvPoint *GetPoint(int Index) const = 0; + virtual const CEnvPointBezier *GetBezier(int Index) const = 0; +}; + +class CMapBasedEnvelopePointAccess : public IEnvelopePointAccess +{ + int m_NumPoints; + CEnvPoint *m_pPoints; + CEnvPointBezier *m_pPointsBezier; + CEnvPointBezier_upstream *m_pPointsBezierUpstream; + +public: + CMapBasedEnvelopePointAccess(class CDataFileReader *pReader); + CMapBasedEnvelopePointAccess(class IMap *pMap); + int NumPoints() const override; + const CEnvPoint *GetPoint(int Index) const override; + const CEnvPointBezier *GetBezier(int Index) const override; +}; + typedef void (*ENVELOPE_EVAL)(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); class CRenderTools @@ -117,7 +142,7 @@ public: void RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f); // map render methods (render_map.cpp) - static void RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result); + static void RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result); void RenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser); void ForceRenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser, float Alpha = 1.0f); void RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset); diff --git a/src/game/client/render_map.cpp b/src/game/client/render_map.cpp index 4c7c43349..92defd4ab 100644 --- a/src/game/client/render_map.cpp +++ b/src/game/client/render_map.cpp @@ -3,23 +3,225 @@ #include #include +#include #include #include +#include +#include #include "render.h" #include #include +#include #include #include using namespace std::chrono_literals; -void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result) +CMapBasedEnvelopePointAccess::CMapBasedEnvelopePointAccess(CDataFileReader *pReader) { + bool FoundBezierEnvelope = false; + int EnvStart, EnvNum; + pReader->GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); + for(int EnvIndex = 0; EnvIndex < EnvNum; EnvIndex++) + { + CMapItemEnvelope *pEnvelope = static_cast(pReader->GetItem(EnvStart + EnvIndex)); + if(pEnvelope->m_Version >= CMapItemEnvelope_v3::CURRENT_VERSION) + { + FoundBezierEnvelope = true; + break; + } + } + + if(FoundBezierEnvelope) + { + m_pPoints = nullptr; + m_pPointsBezier = nullptr; + + int EnvPointStart, FakeEnvPointNum; + pReader->GetType(MAPITEMTYPE_ENVPOINTS, &EnvPointStart, &FakeEnvPointNum); + if(FakeEnvPointNum > 0) + m_pPointsBezierUpstream = static_cast(pReader->GetItem(EnvPointStart)); + else + m_pPointsBezierUpstream = nullptr; + + m_NumPoints = pReader->GetItemSize(EnvPointStart) / sizeof(CEnvPointBezier_upstream); + } + else + { + int EnvPointStart, FakeEnvPointNum; + pReader->GetType(MAPITEMTYPE_ENVPOINTS, &EnvPointStart, &FakeEnvPointNum); + if(FakeEnvPointNum > 0) + m_pPoints = static_cast(pReader->GetItem(EnvPointStart)); + else + m_pPoints = nullptr; + + m_NumPoints = pReader->GetItemSize(EnvPointStart) / sizeof(CEnvPoint); + + int EnvPointBezierStart, FakeEnvPointBezierNum; + pReader->GetType(MAPITEMTYPE_ENVPOINTS_BEZIER, &EnvPointBezierStart, &FakeEnvPointBezierNum); + const int NumPointsBezier = pReader->GetItemSize(EnvPointBezierStart) / sizeof(CEnvPointBezier); + if(FakeEnvPointBezierNum > 0 && m_NumPoints == NumPointsBezier) + m_pPointsBezier = static_cast(pReader->GetItem(EnvPointBezierStart)); + else + m_pPointsBezier = nullptr; + + m_pPointsBezierUpstream = nullptr; + } +} + +CMapBasedEnvelopePointAccess::CMapBasedEnvelopePointAccess(IMap *pMap) : + CMapBasedEnvelopePointAccess(static_cast(pMap)->GetReader()) +{ +} + +int CMapBasedEnvelopePointAccess::NumPoints() const +{ + return m_NumPoints; +} + +const CEnvPoint *CMapBasedEnvelopePointAccess::GetPoint(int Index) const +{ + if(Index < 0 || Index >= m_NumPoints) + return nullptr; + if(m_pPoints != nullptr) + return &m_pPoints[Index]; + if(m_pPointsBezierUpstream != nullptr) + return &m_pPointsBezierUpstream[Index]; + return nullptr; +} + +const CEnvPointBezier *CMapBasedEnvelopePointAccess::GetBezier(int Index) const +{ + if(Index < 0 || Index >= m_NumPoints) + return nullptr; + if(m_pPointsBezier != nullptr) + return &m_pPointsBezier[Index]; + if(m_pPointsBezierUpstream != nullptr) + return &m_pPointsBezierUpstream[Index].m_Bezier; + return nullptr; +} + +static void ValidateFCurve(const vec2 &p0, vec2 &p1, vec2 &p2, const vec2 &p3) +{ + // validate the bezier curve + p1.x = clamp(p1.x, p0.x, p3.x); + p2.x = clamp(p2.x, p0.x, p3.x); +} + +static double CubicRoot(double x) +{ + if(x == 0.0) + return 0.0; + else if(x < 0.0) + return -std::exp(std::log(-x) / 3.0); + else + return std::exp(std::log(x) / 3.0); +} + +static float SolveBezier(float x, float p0, float p1, float p2, float p3) +{ + // check for valid f-curve + // we only take care of monotonic bezier curves, so there has to be exactly 1 real solution + if(!(p0 <= x && x <= p3) || !(p0 <= p1 && p1 <= p3) || !(p0 <= p2 && p2 <= p3)) + return 0.0f; + + const double x3 = -p0 + 3.0 * p1 - 3.0 * p2 + p3; + const double x2 = 3.0 * p0 - 6.0 * p1 + 3.0 * p2; + const double x1 = -3.0 * p0 + 3.0 * p1; + const double x0 = p0 - x; + + if(x3 == 0.0 && x2 == 0.0) + { + // linear + // a * t + b = 0 + const double a = x1; + const double b = x0; + + if(a == 0.0) + return 0.0f; + return -b / a; + } + else if(x3 == 0.0) + { + // quadratic + // t * t + b * t +c = 0 + const double b = x1 / x2; + const double c = x0 / x2; + + if(c == 0.0) + return 0.0f; + + const double D = b * b - 4.0 * c; + const double SqrtD = std::sqrt(D); + + const double t = (-b + SqrtD) / 2.0; + + if(0.0 <= t && t <= 1.0001f) + return t; + return (-b - SqrtD) / 2.0; + } + else + { + // cubic + // t * t * t + a * t * t + b * t * t + c = 0 + const double a = x2 / x3; + const double b = x1 / x3; + const double c = x0 / x3; + + // substitute t = y - a / 3 + const double sub = a / 3.0; + + // depressed form x^3 + px + q = 0 + // cardano's method + const double p = b / 3.0 - a * a / 9.0; + const double q = (2.0 * a * a * a / 27.0 - a * b / 3.0 + c) / 2.0; + + const double D = q * q + p * p * p; + + if(D > 0.0) + { + // only one 'real' solution + const double s = std::sqrt(D); + return CubicRoot(s - q) - CubicRoot(s + q) - sub; + } + else if(D == 0.0) + { + // one single, one double solution or triple solution + const double s = CubicRoot(-q); + const double t = 2.0 * s - sub; + + if(0.0 <= t && t <= 1.0001f) + return t; + return (-s - sub); + } + else + { + // Casus irreductibilis ... ,_, + const double phi = std::acos(-q / std::sqrt(-(p * p * p))) / 3.0; + const double s = 2.0 * std::sqrt(-p); + + const double t1 = s * std::cos(phi) - sub; + + if(0.0 <= t1 && t1 <= 1.0001f) + return t1; + + const double t2 = -s * std::cos(phi + pi / 3.0) - sub; + + if(0.0 <= t2 && t2 <= 1.0001f) + return t2; + return -s * std::cos(phi - pi / 3.0) - sub; + } + } +} + +void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result) +{ + const int NumPoints = pPoints->NumPoints(); if(NumPoints == 0) { Result = ColorRGBA(); @@ -28,14 +230,16 @@ void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Cha if(NumPoints == 1) { - Result.r = fx2f(pPoints[0].m_aValues[0]); - Result.g = fx2f(pPoints[0].m_aValues[1]); - Result.b = fx2f(pPoints[0].m_aValues[2]); - Result.a = fx2f(pPoints[0].m_aValues[3]); + const CEnvPoint *pFirstPoint = pPoints->GetPoint(0); + Result.r = fx2f(pFirstPoint->m_aValues[0]); + Result.g = fx2f(pFirstPoint->m_aValues[1]); + Result.b = fx2f(pFirstPoint->m_aValues[2]); + Result.a = fx2f(pFirstPoint->m_aValues[3]); return; } - int64_t MaxPointTime = (int64_t)pPoints[NumPoints - 1].m_Time * std::chrono::nanoseconds(1ms).count(); + const CEnvPoint *pLastPoint = pPoints->GetPoint(NumPoints - 1); + const int64_t MaxPointTime = (int64_t)pLastPoint->m_Time * std::chrono::nanoseconds(1ms).count(); if(MaxPointTime > 0) // TODO: remove this check when implementing a IO check for maps(in this case broken envelopes) TimeNanos = std::chrono::nanoseconds(TimeNanos.count() % MaxPointTime); else @@ -44,31 +248,70 @@ void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Cha int TimeMillis = (int)(TimeNanos / std::chrono::nanoseconds(1ms).count()).count(); for(int i = 0; i < NumPoints - 1; i++) { - if(TimeMillis >= pPoints[i].m_Time && TimeMillis <= pPoints[i + 1].m_Time) + const CEnvPoint *pCurrentPoint = pPoints->GetPoint(i); + const CEnvPoint *pNextPoint = pPoints->GetPoint(i + 1); + if(TimeMillis >= pCurrentPoint->m_Time && TimeMillis <= pNextPoint->m_Time) { - float Delta = pPoints[i + 1].m_Time - pPoints[i].m_Time; - float a = (float)(((double)TimeNanos.count() / (double)std::chrono::nanoseconds(1ms).count()) - pPoints[i].m_Time) / Delta; + const float Delta = pNextPoint->m_Time - pCurrentPoint->m_Time; + float a = (float)(((double)TimeNanos.count() / (double)std::chrono::nanoseconds(1ms).count()) - pCurrentPoint->m_Time) / Delta; - if(pPoints[i].m_Curvetype == CURVETYPE_SMOOTH) - a = -2 * a * a * a + 3 * a * a; // second hermite basis - else if(pPoints[i].m_Curvetype == CURVETYPE_SLOW) + switch(pCurrentPoint->m_Curvetype) + { + case CURVETYPE_STEP: + a = 0.0f; + break; + + case CURVETYPE_SLOW: a = a * a * a; - else if(pPoints[i].m_Curvetype == CURVETYPE_FAST) + break; + + case CURVETYPE_FAST: + a = 1.0f - a; + a = 1.0f - a * a * a; + break; + + case CURVETYPE_SMOOTH: + a = -2.0f * a * a * a + 3.0f * a * a; // second hermite basis + break; + + case CURVETYPE_BEZIER: { - a = 1 - a; - a = 1 - a * a * a; + const CEnvPointBezier *pCurrentPointBezier = pPoints->GetBezier(i); + const CEnvPointBezier *pNextPointBezier = pPoints->GetBezier(i + 1); + if(pCurrentPointBezier == nullptr || pNextPointBezier == nullptr) + break; // fallback to linear + for(int c = 0; c < Channels; c++) + { + // monotonic 2d cubic bezier curve + const vec2 p0 = vec2(pCurrentPoint->m_Time / 1000.0f, fx2f(pCurrentPoint->m_aValues[c])); + const vec2 p3 = vec2(pNextPoint->m_Time / 1000.0f, fx2f(pNextPoint->m_aValues[c])); + + const vec2 OutTang = vec2(pCurrentPointBezier->m_aOutTangentDeltaX[c] / 1000.0f, fx2f(pCurrentPointBezier->m_aOutTangentDeltaY[c])); + const vec2 InTang = -vec2(pNextPointBezier->m_aInTangentDeltaX[c] / 1000.0f, fx2f(pNextPointBezier->m_aInTangentDeltaY[c])); + vec2 p1 = p0 + OutTang; + vec2 p2 = p3 - InTang; + + // validate bezier curve + ValidateFCurve(p0, p1, p2, p3); + + // solve x(a) = time for a + a = clamp(SolveBezier(TimeMillis / 1000.0f, p0.x, p1.x, p2.x, p3.x), 0.0f, 1.0f); + + // value = y(t) + Result[c] = bezier(p0.y, p1.y, p2.y, p3.y, a); + } + return; } - else if(pPoints[i].m_Curvetype == CURVETYPE_STEP) - a = 0; - else - { - // linear + + case CURVETYPE_LINEAR: [[fallthrough]]; + default: + break; } for(int c = 0; c < Channels; c++) { - float v0 = fx2f(pPoints[i].m_aValues[c]); - float v1 = fx2f(pPoints[i + 1].m_aValues[c]); + const float v0 = fx2f(pCurrentPoint->m_aValues[c]); + const float v1 = fx2f(pNextPoint->m_aValues[c]); Result[c] = v0 + (v1 - v0) * a; } @@ -76,10 +319,10 @@ void CRenderTools::RenderEvalEnvelope(CEnvPoint *pPoints, int NumPoints, int Cha } } - Result.r = fx2f(pPoints[NumPoints - 1].m_aValues[0]); - Result.g = fx2f(pPoints[NumPoints - 1].m_aValues[1]); - Result.b = fx2f(pPoints[NumPoints - 1].m_aValues[2]); - Result.a = fx2f(pPoints[NumPoints - 1].m_aValues[3]); + Result.r = fx2f(pLastPoint->m_aValues[0]); + Result.g = fx2f(pLastPoint->m_aValues[1]); + Result.b = fx2f(pLastPoint->m_aValues[2]); + Result.a = fx2f(pLastPoint->m_aValues[3]); } static void Rotate(CPoint *pCenter, CPoint *pPoint, float Rotation) diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 6ec2e96da..d0a6d6a55 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -2116,19 +2116,26 @@ void CEditor::DoQuadEnvelopes(const std::vector &vQuads, IGraphics::CText continue; //QuadParams - const CPoint *pPoints = vQuads[j].m_aPoints; + const CPoint *pPivotPoint = &vQuads[j].m_aPoints[4]; for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size() - 1; i++) { - float OffsetX = fx2f(apEnvelope[j]->m_vPoints[i].m_aValues[0]); - float OffsetY = fx2f(apEnvelope[j]->m_vPoints[i].m_aValues[1]); - vec2 Pos0 = vec2(fx2f(pPoints[4].x) + OffsetX, fx2f(pPoints[4].y) + OffsetY); + ColorRGBA Result; + apEnvelope[j]->Eval(apEnvelope[j]->m_vPoints[i].m_Time / 1000.0f + 0.000001f, Result); + vec2 Pos0 = vec2(fx2f(pPivotPoint->x) + Result.r, fx2f(pPivotPoint->y) + Result.g); - OffsetX = fx2f(apEnvelope[j]->m_vPoints[i + 1].m_aValues[0]); - OffsetY = fx2f(apEnvelope[j]->m_vPoints[i + 1].m_aValues[1]); - vec2 Pos1 = vec2(fx2f(pPoints[4].x) + OffsetX, fx2f(pPoints[4].y) + OffsetY); + const int Steps = 15; + for(int n = 1; n <= Steps; n++) + { + const float Time = mix(apEnvelope[j]->m_vPoints[i].m_Time, apEnvelope[j]->m_vPoints[i + 1].m_Time, (float)n / Steps); + apEnvelope[j]->Eval(Time / 1000.0f - 0.000001f, Result); - IGraphics::CLineItem Line = IGraphics::CLineItem(Pos0.x, Pos0.y, Pos1.x, Pos1.y); - Graphics()->LinesDraw(&Line, 1); + vec2 Pos1 = vec2(fx2f(pPivotPoint->x) + Result.r, fx2f(pPivotPoint->y) + Result.g); + + IGraphics::CLineItem Line = IGraphics::CLineItem(Pos0.x, Pos0.y, Pos1.x, Pos1.y); + Graphics()->LinesDraw(&Line, 1); + + Pos0 = Pos1; + } } } Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); @@ -5528,7 +5535,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(pNewEnv) // add the default points { - if(pNewEnv->m_Channels == 4) + if(pNewEnv->GetChannels() == 4) { pNewEnv->AddPoint(0, f2fx(1.0f), f2fx(1.0f), f2fx(1.0f), f2fx(1.0f)); pNewEnv->AddPoint(1000, f2fx(1.0f), f2fx(1.0f), f2fx(1.0f), f2fx(1.0f)); @@ -5600,7 +5607,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } bool ShowColorBar = false; - if(pEnvelope && pEnvelope->m_Channels == 4) + if(pEnvelope && pEnvelope->GetChannels() == 4) { ShowColorBar = true; View.HSplitTop(20.0f, &ColorBar, &View); @@ -5612,7 +5619,6 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(pEnvelope) { - static std::vector s_vSelection; static int s_EnvelopeEditorID = 0; static int s_ActiveChannels = 0xf; @@ -5642,17 +5648,17 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) for(int i = 0; i < CEnvPoint::MAX_CHANNELS; i++, Bit <<= 1) { ToolBar.VSplitLeft(15.0f, &Button, &ToolBar); - if(i < pEnvelope->m_Channels) + if(i < pEnvelope->GetChannels()) { int Corners = IGraphics::CORNER_NONE; - if(pEnvelope->m_Channels == 1) + if(pEnvelope->GetChannels() == 1) Corners = IGraphics::CORNER_ALL; else if(i == 0) Corners = IGraphics::CORNER_L; - else if(i == pEnvelope->m_Channels - 1) + else if(i == pEnvelope->GetChannels() - 1) Corners = IGraphics::CORNER_R; - if(DoButton_Env(&s_aChannelButtons[i], s_aapNames[pEnvelope->m_Channels - 1][i], s_ActiveChannels & Bit, &Button, s_aapDescriptions[pEnvelope->m_Channels - 1][i], aColors[i], Corners)) + if(DoButton_Env(&s_aChannelButtons[i], s_aapNames[pEnvelope->GetChannels() - 1][i], s_ActiveChannels & Bit, &Button, s_aapDescriptions[pEnvelope->GetChannels() - 1][i], aColors[i], Corners)) s_ActiveChannels ^= Bit; } } @@ -5706,12 +5712,65 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) m_pTooltip = "Press right mouse button to create a new point"; } + // keep track of selected point to handle value/time text input + static const void *s_pSelectedPoint = nullptr; + + // render tangents for bezier curves + { + UI()->ClipEnable(&View); + Graphics()->TextureClear(); + Graphics()->LinesBegin(); + for(int c = 0; c < pEnvelope->GetChannels(); c++) + { + if(!(s_ActiveChannels & (1 << c))) + continue; + + for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++) + { + const float PosX = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; + const float PosY = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); + + // Out-Tangent + if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER) + { + const float OutTangentX = PosX + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] / 1000.0f / EndTime; + const float OutTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]) / (Top - Bottom); + + if(s_pSelectedPoint == &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f); + else + Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 0.4f); + + IGraphics::CLineItem LineItem(View.x + PosX * View.w, View.y + View.h - PosY * View.h, View.x + OutTangentX * View.w, View.y + View.h - OutTangentY * View.h); + Graphics()->LinesDraw(&LineItem, 1); + } + + // In-Tangent + if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER) + { + const float InTangentX = PosX + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] / 1000.0f / EndTime; + const float InTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]) / (Top - Bottom); + + if(s_pSelectedPoint == &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f); + else + Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 0.4f); + + IGraphics::CLineItem LineItem(View.x + PosX * View.w, View.y + View.h - PosY * View.h, View.x + InTangentX * View.w, View.y + View.h - InTangentY * View.h); + Graphics()->LinesDraw(&LineItem, 1); + } + } + } + Graphics()->LinesEnd(); + UI()->ClipDisable(); + } + // render lines { UI()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->LinesBegin(); - for(int c = 0; c < pEnvelope->m_Channels; c++) + for(int c = 0; c < pEnvelope->GetChannels(); c++) { if(s_ActiveChannels & (1 << c)) Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 1); @@ -5748,19 +5807,18 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) float t0 = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; float t1 = pEnvelope->m_vPoints[i + 1].m_Time / 1000.0f / EndTime; - CUIRect v; - v.x = CurveBar.x + (t0 + (t1 - t0) * 0.5f) * CurveBar.w; - v.y = CurveBar.y; - v.h = CurveBar.h; - v.w = CurveBar.h; - v.x -= v.w / 2; - void *pID = &pEnvelope->m_vPoints[i].m_Curvetype; - const char *apTypeName[] = { - "N", "L", "S", "F", "M"}; - const char *pTypeName = "Invalid"; + CUIRect CurveButton; + CurveButton.x = CurveBar.x + (t0 + (t1 - t0) * 0.5f) * CurveBar.w; + CurveButton.y = CurveBar.y; + CurveButton.h = CurveBar.h; + CurveButton.w = CurveBar.h; + CurveButton.x -= CurveButton.w / 2.0f; + const void *pID = &pEnvelope->m_vPoints[i].m_Curvetype; + const char *apTypeName[] = {"N", "L", "S", "F", "M", "B"}; + const char *pTypeName = "!?"; if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(apTypeName)) pTypeName = apTypeName[pEnvelope->m_vPoints[i].m_Curvetype]; - if(DoButton_Editor(pID, pTypeName, 0, &v, 0, "Switch curve type")) + if(DoButton_Editor(pID, pTypeName, 0, &CurveButton, 0, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)")) pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES; } } @@ -5798,15 +5856,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) // render handles - // keep track of last Env - static void *s_pID = nullptr; - static CLineInputNumber s_CurValueInput; static CLineInputNumber s_CurTimeInput; if(CurrentEnvelopeSwitched) { - s_pID = nullptr; + s_pSelectedPoint = nullptr; // update displayed text s_CurValueInput.SetFloat(0.0f); @@ -5814,149 +5869,351 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } { - int CurrentValue = 0, CurrentTime = 0; - Graphics()->TextureClear(); Graphics()->QuadsBegin(); - for(int c = 0; c < pEnvelope->m_Channels; c++) + for(int c = 0; c < pEnvelope->GetChannels(); c++) { if(!(s_ActiveChannels & (1 << c))) continue; for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++) { - float x0 = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; - float y0 = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); - CUIRect Final; - Final.x = View.x + x0 * View.w; - Final.y = View.y + View.h - y0 * View.h; - Final.x -= 2.0f; - Final.y -= 2.0f; - Final.w = 4.0f; - Final.h = 4.0f; - - void *pID = &pEnvelope->m_vPoints[i].m_aValues[c]; - - if(UI()->MouseInside(&Final)) - UI()->SetHotItem(pID); - - float ColorMod = 1.0f; - - if(UI()->CheckActiveItem(pID)) + // point handle { - if(!UI()->MouseButton(0)) - { - m_SelectedQuadEnvelope = -1; - m_SelectedEnvelopePoint = -1; + const float PosX = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; + const float PosY = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); + CUIRect Final; + Final.x = View.x + PosX * View.w; + Final.y = View.y + View.h - PosY * View.h; + Final.x -= 2.0f; + Final.y -= 2.0f; + Final.w = 4.0f; + Final.h = 4.0f; - UI()->SetActiveItem(nullptr); - } - else + const void *pID = &pEnvelope->m_vPoints[i].m_aValues[c]; + + if(UI()->MouseInside(&Final)) + UI()->SetHotItem(pID); + + float ColorMod = 1.0f; + + if(UI()->CheckActiveItem(pID)) { - if(Input()->ShiftIsPressed()) + if(!UI()->MouseButton(0)) { - if(i != 0) - { - if(Input()->ModifierIsPressed()) - pEnvelope->m_vPoints[i].m_Time += (int)((m_MouseDeltaX)); - else - pEnvelope->m_vPoints[i].m_Time += (int)((m_MouseDeltaX * TimeScale) * 1000.0f); - if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; - if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; - } + m_SelectedQuadEnvelope = -1; + m_SelectedEnvelopePoint = -1; + + UI()->SetActiveItem(nullptr); } else { - if(Input()->ModifierIsPressed()) - pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * 0.001f); + if(Input()->ShiftIsPressed()) + { + if(i != 0) + { + float DeltaX = m_MouseDeltaX * TimeScale * (Input()->ModifierIsPressed() ? 1.0f : 1000.0f); + DeltaX = DeltaX < 0 ? -std::ceil(-DeltaX) : std::ceil(DeltaX); + pEnvelope->m_vPoints[i].m_Time += (int)DeltaX; + + if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; + if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; + } + } else - pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * ValueScale); + { + pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * (Input()->ModifierIsPressed() ? 0.001f : ValueScale)); + } + + m_SelectedQuadEnvelope = m_SelectedEnvelope; + m_ShowEnvelopePreview = SHOWENV_SELECTED; + m_SelectedEnvelopePoint = i; + m_Map.OnModify(); } - m_SelectedQuadEnvelope = m_SelectedEnvelope; - m_ShowEnvelopePreview = SHOWENV_SELECTED; - m_SelectedEnvelopePoint = i; - m_Map.OnModify(); + ColorMod = 100.0f; + Graphics()->SetColor(1, 1, 1, 1); } - - ColorMod = 100.0f; - Graphics()->SetColor(1, 1, 1, 1); - } - else if(UI()->HotItem() == pID) - { - if(UI()->MouseButton(0)) + else if(UI()->HotItem() == pID) { - s_vSelection.clear(); - s_vSelection.push_back(i); - UI()->SetActiveItem(pID); - // track it - s_pID = pID; - } - - // remove point - if(UI()->MouseButtonClicked(1)) - { - if(s_pID == pID) + if(UI()->MouseButton(0)) { - s_pID = nullptr; + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + } + + // remove point + if(UI()->MouseButtonClicked(1)) + { + if(s_pSelectedPoint == pID) + { + s_pSelectedPoint = nullptr; + + // update displayed text + s_CurValueInput.SetFloat(0.0f); + s_CurTimeInput.SetFloat(0.0f); + } + + pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); + m_Map.OnModify(); + } + + m_ShowEnvelopePreview = SHOWENV_SELECTED; + ColorMod = 100.0f; + Graphics()->SetColor(1, 0.75f, 0.75f, 1); + m_pTooltip = "Envelope point. Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time point instead of value. Right click to delete."; + } + + if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + { + if(i != 0) + { + pEnvelope->m_vPoints[i].m_Time = s_CurTimeInput.GetFloat() * 1000.0f; + + if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; + if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) + pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; + } + else + pEnvelope->m_vPoints[i].m_Time = 0.0f; + + s_CurTimeInput.SetFloat(pEnvelope->m_vPoints[i].m_Time / 1000.0f); + + pEnvelope->m_vPoints[i].m_aValues[c] = f2fx(s_CurValueInput.GetFloat()); + s_CurValueInput.SetFloat(fx2f(pEnvelope->m_vPoints[i].m_aValues[c])); + } + + if(UI()->CheckActiveItem(pID)) + { + const int CurrentTime = pEnvelope->m_vPoints[i].m_Time; + const int CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c]; + + // update displayed text + s_CurValueInput.SetFloat(fx2f(CurrentValue)); + s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); + } + + if(pID == s_pSelectedPoint || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + else + Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 1.0f); + IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h); + Graphics()->QuadsDrawTL(&QuadItem, 1); + } + + // tangent handles for bezier curves + { + const float PosX = pEnvelope->m_vPoints[i].m_Time / 1000.0f / EndTime; + const float PosY = (fx2f(pEnvelope->m_vPoints[i].m_aValues[c]) - Bottom) / (Top - Bottom); + + // Out-Tangent handle + if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER) + { + const float OutTangentX = PosX + (pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] / 1000.0f / EndTime); + const float OutTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]) / (Top - Bottom); + + CUIRect Final; + Final.x = View.x + OutTangentX * View.w; + Final.y = View.y + View.h - OutTangentY * View.h; + Final.x -= 2.0f; + Final.y -= 2.0f; + Final.w = 4.0f; + Final.h = 4.0f; + + // handle logic + bool Updated = false; + const void *pID = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]; + + float ColorMod = 1.0f; + if(UI()->MouseInside(&Final)) + UI()->SetHotItem(pID); + + if(UI()->CheckActiveItem(pID)) + { + if(!UI()->MouseButton(0)) + { + m_SelectedQuadEnvelope = -1; + m_SelectedEnvelopePoint = -1; + + UI()->SetActiveItem(nullptr); + } + else + { + float DeltaX = m_MouseDeltaX * TimeScale * (Input()->ModifierIsPressed() ? 1.0f : 1000.0f); + DeltaX = DeltaX < 0 ? -std::ceil(-DeltaX) : std::ceil(DeltaX); + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] += (int)DeltaX; + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] -= f2fx(m_MouseDeltaY * (Input()->ModifierIsPressed() ? 0.005f : ValueScale)); + + // clamp time value + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = clamp(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c], 0, EndTime * 1000.0f - pEnvelope->m_vPoints[i].m_Time); + + m_SelectedQuadEnvelope = m_SelectedEnvelope; + m_ShowEnvelopePreview = SHOWENV_SELECTED; + m_SelectedEnvelopePoint = i; + m_Map.OnModify(); + } + ColorMod = 100.0f; + } + else if(UI()->HotItem() == pID) + { + if(UI()->MouseButton(0)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + } + + // reset + if(UI()->MouseButtonClicked(1)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX)); + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY)); + m_Map.OnModify(); + Updated = true; + } + + m_ShowEnvelopePreview = SHOWENV_SELECTED; + ColorMod = 100.0f; + m_pTooltip = "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Right click to reset."; + } + + if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + { + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = clamp(s_CurTimeInput.GetFloat() * 1000.0f - pEnvelope->m_vPoints[i].m_Time, 0, EndTime * 1000.0f - pEnvelope->m_vPoints[i].m_Time); + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = f2fx(s_CurValueInput.GetFloat()) - pEnvelope->m_vPoints[i].m_aValues[c]; + Updated = true; + } + + if(UI()->CheckActiveItem(pID) || Updated) + { + const int CurrentTime = pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]; + const int CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]; // update displayed text - s_CurValueInput.SetFloat(0.0f); - s_CurTimeInput.SetFloat(0.0f); + s_CurValueInput.SetFloat(fx2f(CurrentValue)); + s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); } - pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); - m_Map.OnModify(); + if(pID == s_pSelectedPoint || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); + else + Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 0.5f); + + // draw triangle + IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h); + Graphics()->QuadsDrawFreeform(&FreeformItem, 1); } - m_ShowEnvelopePreview = SHOWENV_SELECTED; - ColorMod = 100.0f; - Graphics()->SetColor(1, 0.75f, 0.75f, 1); - m_pTooltip = "Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time point as well. Right click to delete."; - } - - if(pID == s_pID && (Input()->KeyIsPressed(KEY_RETURN) || Input()->KeyIsPressed(KEY_KP_ENTER))) - { - if(i != 0) + // In-Tangent handle + if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER) { - pEnvelope->m_vPoints[i].m_Time = s_CurTimeInput.GetFloat() * 1000.0f; + const float InTangentX = PosX + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] / 1000.0f / EndTime; + const float InTangentY = PosY + fx2f(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]) / (Top - Bottom); - if(pEnvelope->m_vPoints[i].m_Time < pEnvelope->m_vPoints[i - 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i - 1].m_Time + 1; - if(i + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[i].m_Time > pEnvelope->m_vPoints[i + 1].m_Time) - pEnvelope->m_vPoints[i].m_Time = pEnvelope->m_vPoints[i + 1].m_Time - 1; + CUIRect Final; + Final.x = View.x + InTangentX * View.w; + Final.y = View.y + View.h - InTangentY * View.h; + Final.x -= 2.0f; + Final.y -= 2.0f; + Final.w = 4.0f; + Final.h = 4.0f; + + // handle logic + bool Updated = false; + const void *pID = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]; + + float ColorMod = 1.0f; + if(UI()->MouseInside(&Final)) + UI()->SetHotItem(pID); + + if(UI()->CheckActiveItem(pID)) + { + if(!UI()->MouseButton(0)) + { + m_SelectedQuadEnvelope = -1; + m_SelectedEnvelopePoint = -1; + + UI()->SetActiveItem(nullptr); + } + else + { + float DeltaX = m_MouseDeltaX * TimeScale * (Input()->ModifierIsPressed() ? 1.0f : 1000.0f); + DeltaX = DeltaX < 0 ? -std::ceil(-DeltaX) : std::ceil(DeltaX); + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] += (int)DeltaX; + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] -= f2fx(m_MouseDeltaY * (Input()->ModifierIsPressed() ? 0.005f : ValueScale)); + + // clamp time value + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = clamp(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c], -pEnvelope->m_vPoints[i].m_Time, 0); + + m_SelectedQuadEnvelope = m_SelectedEnvelope; + m_ShowEnvelopePreview = SHOWENV_SELECTED; + m_SelectedEnvelopePoint = i; + m_Map.OnModify(); + } + ColorMod = 100.0f; + } + else if(UI()->HotItem() == pID) + { + if(UI()->MouseButton(0)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + } + + // reset + if(UI()->MouseButtonClicked(1)) + { + UI()->SetActiveItem(pID); + s_pSelectedPoint = pID; + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX)); + mem_zero(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY, sizeof(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY)); + m_Map.OnModify(); + Updated = true; + } + + m_ShowEnvelopePreview = SHOWENV_SELECTED; + ColorMod = 100.0f; + m_pTooltip = "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Right click to reset."; + } + + if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + { + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = clamp(s_CurTimeInput.GetFloat() * 1000.0f - pEnvelope->m_vPoints[i].m_Time, -pEnvelope->m_vPoints[i].m_Time, 0); + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = f2fx(s_CurValueInput.GetFloat()) - pEnvelope->m_vPoints[i].m_aValues[c]; + Updated = true; + } + + if(UI()->CheckActiveItem(pID) || Updated) + { + const int CurrentTime = pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]; + const int CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]; + + // update displayed text + s_CurValueInput.SetFloat(fx2f(CurrentValue)); + s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); + } + + if(pID == s_pSelectedPoint || (m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); + else + Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 0.5f); + + // draw triangle + IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h); + Graphics()->QuadsDrawFreeform(&FreeformItem, 1); } - else - pEnvelope->m_vPoints[i].m_Time = 0.0f; - - s_CurTimeInput.SetFloat(pEnvelope->m_vPoints[i].m_Time / 1000.0f); - - pEnvelope->m_vPoints[i].m_aValues[c] = f2fx(s_CurValueInput.GetFloat()); - s_CurValueInput.SetFloat(fx2f(pEnvelope->m_vPoints[i].m_aValues[c])); } - - if(UI()->CheckActiveItem(pID)) - { - CurrentTime = pEnvelope->m_vPoints[i].m_Time; - CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c]; - - // update displayed text - s_CurValueInput.SetFloat(fx2f(CurrentValue)); - s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); - } - - if(m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i) - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); - else - Graphics()->SetColor(aColors[c].r * ColorMod, aColors[c].g * ColorMod, aColors[c].b * ColorMod, 1.0f); - IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h); - Graphics()->QuadsDrawTL(&QuadItem, 1); } } Graphics()->QuadsEnd(); + } + if(s_pSelectedPoint != nullptr) + { CUIRect ToolBar1; CUIRect ToolBar2; ToolBar.VSplitMid(&ToolBar1, &ToolBar2); @@ -5976,8 +6233,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->DoLabel(&Label2, "Time (in s):", 10.0f, TEXTALIGN_MR); } - DoEditBox(&s_CurValueInput, &ToolBar1, 10.0f, IGraphics::CORNER_ALL, "The value of the selected envelope point"); - DoEditBox(&s_CurTimeInput, &ToolBar2, 10.0f, IGraphics::CORNER_ALL, "The time of the selected envelope point"); + DoEditBox(&s_CurValueInput, &ToolBar1, 10.0f, IGraphics::CORNER_ALL, "The value of the selected element"); + DoEditBox(&s_CurTimeInput, &ToolBar2, 10.0f, IGraphics::CORNER_ALL, "The time of the selected element"); } } } diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 858cae526..08ef91eb8 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -45,17 +45,50 @@ enum class CEnvelope { -public: + class CEnvelopePointAccess : public IEnvelopePointAccess + { + std::vector *m_pvPoints; + + public: + CEnvelopePointAccess(std::vector *pvPoints) + { + m_pvPoints = pvPoints; + } + + int NumPoints() const override + { + return m_pvPoints->size(); + } + + const CEnvPoint *GetPoint(int Index) const override + { + if(Index < 0 || (size_t)Index >= m_pvPoints->size()) + return nullptr; + return &m_pvPoints->at(Index); + } + + const CEnvPointBezier *GetBezier(int Index) const override + { + if(Index < 0 || (size_t)Index >= m_pvPoints->size()) + return nullptr; + return &m_pvPoints->at(Index).m_Bezier; + } + }; + int m_Channels; - std::vector m_vPoints; + +public: + std::vector m_vPoints; + CEnvelopePointAccess m_PointsAccess; char m_aName[32]; float m_Bottom, m_Top; bool m_Synchronized; - CEnvelope(int Chan) + CEnvelope(int Channels) : + m_PointsAccess(&m_vPoints) { - m_Channels = Chan; - m_aName[0] = 0; + SetChannels(Channels); + m_aName[0] = '\0'; m_Bottom = 0; m_Top = 0; m_Synchronized = false; @@ -71,46 +104,82 @@ public: { m_Top = -1000000000.0f; m_Bottom = 1000000000.0f; + CEnvPoint_runtime *pPrevPoint = nullptr; for(auto &Point : m_vPoints) { for(int c = 0; c < m_Channels; c++) { if(ChannelMask & (1 << c)) { - float v = fx2f(Point.m_aValues[c]); - if(v > m_Top) - m_Top = v; - if(v < m_Bottom) - m_Bottom = v; + { + // value handle + const float v = fx2f(Point.m_aValues[c]); + m_Top = maximum(m_Top, v); + m_Bottom = minimum(m_Bottom, v); + } + + if(Point.m_Curvetype == CURVETYPE_BEZIER) + { + // out-tangent handle + const float v = fx2f(Point.m_aValues[c] + Point.m_Bezier.m_aOutTangentDeltaY[c]); + m_Top = maximum(m_Top, v); + m_Bottom = minimum(m_Bottom, v); + } + + if(pPrevPoint != nullptr && pPrevPoint->m_Curvetype == CURVETYPE_BEZIER) + { + // in-tangent handle + const float v = fx2f(Point.m_aValues[c] + Point.m_Bezier.m_aInTangentDeltaY[c]); + m_Top = maximum(m_Top, v); + m_Bottom = minimum(m_Bottom, v); + } } } + pPrevPoint = &Point; } } int Eval(float Time, ColorRGBA &Color) { - CRenderTools::RenderEvalEnvelope(&m_vPoints[0], m_vPoints.size(), m_Channels, std::chrono::nanoseconds((int64_t)((double)Time * (double)std::chrono::nanoseconds(1s).count())), Color); + CRenderTools::RenderEvalEnvelope(&m_PointsAccess, m_Channels, std::chrono::nanoseconds((int64_t)((double)Time * (double)std::chrono::nanoseconds(1s).count())), Color); return m_Channels; } void AddPoint(int Time, int v0, int v1 = 0, int v2 = 0, int v3 = 0) { - CEnvPoint p; + CEnvPoint_runtime p; p.m_Time = Time; p.m_aValues[0] = v0; p.m_aValues[1] = v1; p.m_aValues[2] = v2; p.m_aValues[3] = v3; p.m_Curvetype = CURVETYPE_LINEAR; + for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++) + { + p.m_Bezier.m_aInTangentDeltaX[c] = 0; + p.m_Bezier.m_aInTangentDeltaY[c] = 0; + p.m_Bezier.m_aOutTangentDeltaX[c] = 0; + p.m_Bezier.m_aOutTangentDeltaY[c] = 0; + } m_vPoints.push_back(p); Resort(); } float EndTime() const { - if(!m_vPoints.empty()) - return m_vPoints[m_vPoints.size() - 1].m_Time * (1.0f / 1000.0f); - return 0; + if(m_vPoints.empty()) + return 0.0f; + return m_vPoints.back().m_Time / 1000.0f; + } + + int GetChannels() const + { + return m_Channels; + } + + void SetChannels(int Channels) + { + m_Channels = clamp(Channels, 1, CEnvPoint::MAX_CHANNELS); } }; diff --git a/src/game/editor/io.cpp b/src/game/editor/io.cpp index 90253afd5..02ecd8906 100644 --- a/src/game/editor/io.cpp +++ b/src/game/editor/io.cpp @@ -345,12 +345,13 @@ bool CEditorMap::Save(const char *pFileName) } // save envelopes + m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving envelopes"); int PointCount = 0; for(size_t e = 0; e < m_vpEnvelopes.size(); e++) { CMapItemEnvelope Item; Item.m_Version = CMapItemEnvelope::CURRENT_VERSION; - Item.m_Channels = m_vpEnvelopes[e]->m_Channels; + Item.m_Channels = m_vpEnvelopes[e]->GetChannels(); Item.m_StartPoint = PointCount; Item.m_NumPoints = m_vpEnvelopes[e]->m_vPoints.size(); Item.m_Synchronized = m_vpEnvelopes[e]->m_Synchronized; @@ -361,20 +362,61 @@ bool CEditorMap::Save(const char *pFileName) } // save points - int TotalSize = sizeof(CEnvPoint) * PointCount; - CEnvPoint *pPoints = (CEnvPoint *)calloc(maximum(PointCount, 1), sizeof(*pPoints)); + m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving envelope points"); + bool BezierUsed = true; + for(const auto &pEnvelope : m_vpEnvelopes) + { + for(const auto &Point : pEnvelope->m_vPoints) + { + if(Point.m_Curvetype == CURVETYPE_BEZIER) + { + BezierUsed = true; + break; + } + } + if(BezierUsed) + break; + } + + CEnvPoint *pPoints = (CEnvPoint *)calloc(maximum(PointCount, 1), sizeof(CEnvPoint)); + CEnvPointBezier *pPointsBezier = nullptr; + if(BezierUsed) + pPointsBezier = (CEnvPointBezier *)calloc(maximum(PointCount, 1), sizeof(CEnvPointBezier)); PointCount = 0; for(const auto &pEnvelope : m_vpEnvelopes) { - int Count = pEnvelope->m_vPoints.size(); - mem_copy(&pPoints[PointCount], pEnvelope->m_vPoints.data(), sizeof(CEnvPoint) * Count); - PointCount += Count; + const CEnvPoint_runtime *pPrevPoint = nullptr; + for(const auto &Point : pEnvelope->m_vPoints) + { + mem_copy(&pPoints[PointCount], &Point, sizeof(CEnvPoint)); + if(pPointsBezier != nullptr) + { + if(Point.m_Curvetype == CURVETYPE_BEZIER) + { + mem_copy(&pPointsBezier[PointCount].m_aOutTangentDeltaX, &Point.m_Bezier.m_aOutTangentDeltaX, sizeof(Point.m_Bezier.m_aOutTangentDeltaX)); + mem_copy(&pPointsBezier[PointCount].m_aOutTangentDeltaY, &Point.m_Bezier.m_aOutTangentDeltaY, sizeof(Point.m_Bezier.m_aOutTangentDeltaY)); + } + if(pPrevPoint != nullptr && pPrevPoint->m_Curvetype == CURVETYPE_BEZIER) + { + mem_copy(&pPointsBezier[PointCount].m_aInTangentDeltaX, &Point.m_Bezier.m_aInTangentDeltaX, sizeof(Point.m_Bezier.m_aInTangentDeltaX)); + mem_copy(&pPointsBezier[PointCount].m_aInTangentDeltaY, &Point.m_Bezier.m_aInTangentDeltaY, sizeof(Point.m_Bezier.m_aInTangentDeltaY)); + } + } + PointCount++; + pPrevPoint = &Point; + } } - df.AddItem(MAPITEMTYPE_ENVPOINTS, 0, TotalSize, pPoints); + df.AddItem(MAPITEMTYPE_ENVPOINTS, 0, sizeof(CEnvPoint) * PointCount, pPoints); free(pPoints); + if(pPointsBezier != nullptr) + { + df.AddItem(MAPITEMTYPE_ENVPOINTS_BEZIER, 0, sizeof(CEnvPointBezier) * PointCount, pPointsBezier); + free(pPointsBezier); + } + // finish the data file std::shared_ptr pWriterFinishJob = std::make_shared(pFileName, std::move(df)); m_pEditor->Engine()->AddJob(pWriterFinishJob); @@ -904,37 +946,38 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio // load envelopes { - CEnvPoint *pPoints = nullptr; + const CMapBasedEnvelopePointAccess EnvelopePoints(&DataFile); + int EnvStart, EnvNum; + DataFile.GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); + for(int e = 0; e < EnvNum; e++) { - int Start, Num; - DataFile.GetType(MAPITEMTYPE_ENVPOINTS, &Start, &Num); - if(Num) - pPoints = (CEnvPoint *)DataFile.GetItem(Start); - } - - int Start, Num; - DataFile.GetType(MAPITEMTYPE_ENVELOPE, &Start, &Num); - for(int e = 0; e < Num; e++) - { - CMapItemEnvelope *pItem = (CMapItemEnvelope *)DataFile.GetItem(Start + e); + CMapItemEnvelope *pItem = (CMapItemEnvelope *)DataFile.GetItem(EnvStart + e); CEnvelope *pEnv = new CEnvelope(pItem->m_Channels); pEnv->m_vPoints.resize(pItem->m_NumPoints); - mem_copy(pEnv->m_vPoints.data(), &pPoints[pItem->m_StartPoint], sizeof(CEnvPoint) * pItem->m_NumPoints); + for(int p = 0; p < pItem->m_NumPoints; p++) + { + const CEnvPoint *pPoint = EnvelopePoints.GetPoint(pItem->m_StartPoint + p); + if(pPoint != nullptr) + mem_copy(&pEnv->m_vPoints[p], pPoint, sizeof(CEnvPoint)); + const CEnvPointBezier *pPointBezier = EnvelopePoints.GetBezier(pItem->m_StartPoint + p); + if(pPointBezier != nullptr) + mem_copy(&pEnv->m_vPoints[p].m_Bezier, pPointBezier, sizeof(CEnvPointBezier)); + } if(pItem->m_aName[0] != -1) // compatibility with old maps IntsToStr(pItem->m_aName, sizeof(pItem->m_aName) / sizeof(int), pEnv->m_aName); m_vpEnvelopes.push_back(pEnv); - if(pItem->m_Version >= 2) + if(pItem->m_Version >= CMapItemEnvelope_v2::CURRENT_VERSION) pEnv->m_Synchronized = pItem->m_Synchronized; } } { - int Start, Num; - DataFile.GetType(MAPITEMTYPE_AUTOMAPPER_CONFIG, &Start, &Num); - for(int i = 0; i < Num; i++) + int AutomapperConfigStart, AutomapperConfigNum; + DataFile.GetType(MAPITEMTYPE_AUTOMAPPER_CONFIG, &AutomapperConfigStart, &AutomapperConfigNum); + for(int i = 0; i < AutomapperConfigNum; i++) { - CMapItemAutoMapperConfig *pItem = (CMapItemAutoMapperConfig *)DataFile.GetItem(Start + i); + CMapItemAutoMapperConfig *pItem = (CMapItemAutoMapperConfig *)DataFile.GetItem(AutomapperConfigStart + i); if(pItem->m_Version == CMapItemAutoMapperConfig::CURRENT_VERSION) { if(pItem->m_GroupId >= 0 && (size_t)pItem->m_GroupId < m_vpGroups.size() && diff --git a/src/game/editor/layer_tiles.cpp b/src/game/editor/layer_tiles.cpp index 74db24929..0153f0bbe 100644 --- a/src/game/editor/layer_tiles.cpp +++ b/src/game/editor/layer_tiles.cpp @@ -930,7 +930,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { for(; Index >= -1 && Index < (int)m_pEditor->m_Map.m_vpEnvelopes.size(); Index += Step) { - if(Index == -1 || m_pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 4) + if(Index == -1 || m_pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 4) { m_ColorEnv = Index; break; diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 7584f5a97..8436d2034 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -936,7 +936,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b { for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 3) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 3) { pQuad->m_PosEnv = Index; break; @@ -956,7 +956,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b { for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 4) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 4) { pQuad->m_ColorEnv = Index; break; @@ -1095,7 +1095,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, const int StepDirection = Index < pSource->m_PosEnv ? -1 : 1; for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 3) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 3) { pSource->m_PosEnv = Index; break; @@ -1112,7 +1112,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, const int StepDirection = Index < pSource->m_SoundEnv ? -1 : 1; for(; Index >= -1 && Index < (int)pEditor->m_Map.m_vpEnvelopes.size(); Index += StepDirection) { - if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->m_Channels == 1) + if(Index == -1 || pEditor->m_Map.m_vpEnvelopes[Index]->GetChannels() == 1) { pSource->m_SoundEnv = Index; break; diff --git a/src/game/mapitems.h b/src/game/mapitems.h index 8513a619f..086ce15e1 100644 --- a/src/game/mapitems.h +++ b/src/game/mapitems.h @@ -37,6 +37,7 @@ enum CURVETYPE_SLOW, CURVETYPE_FAST, CURVETYPE_SMOOTH, + CURVETYPE_BEZIER, NUM_CURVETYPES, // game layer tiles @@ -351,6 +352,8 @@ struct CMapItemVersion int m_Version; }; +// Represents basic information about envelope points. +// In upstream Teeworlds, this is only used if all CMapItemEnvelope are version 1 or 2. struct CEnvPoint { enum @@ -359,14 +362,45 @@ struct CEnvPoint }; int m_Time; // in ms - int m_Curvetype; + int m_Curvetype; // CURVETYPE_* constants, any unknown value behaves like CURVETYPE_LINEAR int m_aValues[MAX_CHANNELS]; // 1-4 depending on envelope (22.10 fixed point) bool operator<(const CEnvPoint &Other) const { return m_Time < Other.m_Time; } }; +// Represents additional envelope point information for CURVETYPE_BEZIER. +// In DDNet, these are stored separately in an UUID-based map item. +// In upstream Teeworlds, CEnvPointBezier_upstream is used instead. +struct CEnvPointBezier +{ + // DeltaX in ms and DeltaY as 22.10 fxp + int m_aInTangentDeltaX[CEnvPoint::MAX_CHANNELS]; + int m_aInTangentDeltaY[CEnvPoint::MAX_CHANNELS]; + int m_aOutTangentDeltaX[CEnvPoint::MAX_CHANNELS]; + int m_aOutTangentDeltaY[CEnvPoint::MAX_CHANNELS]; +}; + +// Written to maps on upstream Teeworlds for envelope points including bezier information instead of the basic +// CEnvPoint items, if at least one CMapItemEnvelope with version 3 or higher exists in the map. +struct CEnvPointBezier_upstream : public CEnvPoint +{ + CEnvPointBezier m_Bezier; +}; + +// Used to represent all envelope point information at runtime in editor. +// (Can eventually be different than CEnvPointBezier_upstream) +struct CEnvPoint_runtime : public CEnvPoint +{ + CEnvPointBezier m_Bezier; +}; + struct CMapItemEnvelope_v1 { + enum + { + CURRENT_VERSION = 1, + }; + int m_Version; int m_Channels; int m_StartPoint; @@ -374,15 +408,29 @@ struct CMapItemEnvelope_v1 int m_aName[8]; }; -struct CMapItemEnvelope : public CMapItemEnvelope_v1 +struct CMapItemEnvelope_v2 : public CMapItemEnvelope_v1 { enum { - CURRENT_VERSION = 2 + CURRENT_VERSION = 2, }; + int m_Synchronized; }; +// Only written to maps in upstream Teeworlds. +// If at least one of these exists in a map, the envelope points +// are represented by CEnvPointBezier_upstream instead of CEnvPoint. +struct CMapItemEnvelope_v3 : public CMapItemEnvelope_v2 +{ + enum + { + CURRENT_VERSION = 3, + }; +}; + +typedef CMapItemEnvelope_v2 CMapItemEnvelope; + struct CSoundShape { enum diff --git a/src/game/mapitems_ex_types.h b/src/game/mapitems_ex_types.h index 304d4e5d1..2c6869129 100644 --- a/src/game/mapitems_ex_types.h +++ b/src/game/mapitems_ex_types.h @@ -3,3 +3,4 @@ UUID(MAPITEMTYPE_TEST, "mapitemtype-test@ddnet.tw") UUID(MAPITEMTYPE_AUTOMAPPER_CONFIG, "mapitemtype-automapper-config@ddnet.tw") UUID(MAPITEMTYPE_GROUP_EX, "mapitemtype-group@ddnet.tw") +UUID(MAPITEMTYPE_ENVPOINTS_BEZIER, "mapitemtype-envpoints-bezier@ddnet.tw")