Support bezier envelope curves in maps and editor

Port map and editor support for `CURVETYPE_BEZIER` from upstream, i.e. support bezier curves with configurable in- and out-tangents for every envelope point.

The in- and out-tangents are represented by triangles and can be dragged in the envelope editor like the envelope points.

Support reading and writing the bezier information as a separate UUID-based map item. If the bezier information is not found, bezier will default to linear behavior. Old clients will still be able to read the new maps and ignore the unknown map item. The unknown curvetype will also be handled as linear by old clients.

Allow reading upstream maps that use `CMapItemEnvelope` version 3. On upstream, a different struct is used to store all envelope points including bezier information, which broke compatibility to old clients.

Fix holding Ctrl for slow envelope point editing not working for vertical movement.

Highlight the currently selected element (envelope point or bezier tangent marker) which is being used with the value/time edit boxes.

Hide the value/time edit boxes when no element is selected.
This commit is contained in:
Robert Müller 2023-04-24 23:21:44 +02:00
parent 785f03e73a
commit 4ae0928b47
12 changed files with 927 additions and 231 deletions

View file

@ -27,6 +27,20 @@ constexpr inline T mix(const T a, const T b, TB amount)
return a + (b - a) * amount;
}
template<typename T, typename TB>
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);

View file

@ -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;

View file

@ -12,6 +12,7 @@
#include <game/layers.h>
#include <game/mapitems.h>
#include <game/mapitems_ex.h>
#include <game/client/components/camera.h>
#include <game/client/components/mapimages.h>
@ -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);
}
}

View file

@ -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);

View file

@ -3,23 +3,225 @@
#include <base/math.h>
#include <engine/graphics.h>
#include <engine/map.h>
#include <engine/textrender.h>
#include <engine/shared/config.h>
#include <engine/shared/datafile.h>
#include <engine/shared/map.h>
#include "render.h"
#include <game/generated/client_data.h>
#include <game/mapitems.h>
#include <game/mapitems_ex.h>
#include <chrono>
#include <cmath>
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<CMapItemEnvelope *>(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<CEnvPointBezier_upstream *>(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<CEnvPoint *>(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<CEnvPointBezier *>(pReader->GetItem(EnvPointBezierStart));
else
m_pPointsBezier = nullptr;
m_pPointsBezierUpstream = nullptr;
}
}
CMapBasedEnvelopePointAccess::CMapBasedEnvelopePointAccess(IMap *pMap) :
CMapBasedEnvelopePointAccess(static_cast<CMap *>(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);
}
else if(pPoints[i].m_Curvetype == CURVETYPE_STEP)
a = 0;
else
{
// linear
return;
}
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)

View file

@ -2116,19 +2116,26 @@ void CEditor::DoQuadEnvelopes(const std::vector<CQuad> &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);
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<int> 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,28 +5869,28 @@ 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);
// point handle
{
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 + x0 * View.w;
Final.y = View.y + View.h - y0 * View.h;
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;
void *pID = &pEnvelope->m_vPoints[i].m_aValues[c];
const void *pID = &pEnvelope->m_vPoints[i].m_aValues[c];
if(UI()->MouseInside(&Final))
UI()->SetHotItem(pID);
@ -5857,10 +5912,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View)
{
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);
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)
@ -5869,10 +5924,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View)
}
else
{
if(Input()->ModifierIsPressed())
pEnvelope->m_vPoints[i].m_aValues[c] -= f2fx(m_MouseDeltaY * 0.001f);
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;
@ -5888,19 +5940,16 @@ void CEditor::RenderEnvelopeEditor(CUIRect View)
{
if(UI()->MouseButton(0))
{
s_vSelection.clear();
s_vSelection.push_back(i);
UI()->SetActiveItem(pID);
// track it
s_pID = pID;
s_pSelectedPoint = pID;
}
// remove point
if(UI()->MouseButtonClicked(1))
{
if(s_pID == pID)
if(s_pSelectedPoint == pID)
{
s_pID = nullptr;
s_pSelectedPoint = nullptr;
// update displayed text
s_CurValueInput.SetFloat(0.0f);
@ -5914,10 +5963,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View)
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.";
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_pID && (Input()->KeyIsPressed(KEY_RETURN) || Input()->KeyIsPressed(KEY_KP_ENTER)))
if(pID == s_pSelectedPoint && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))
{
if(i != 0)
{
@ -5939,24 +5988,232 @@ void CEditor::RenderEnvelopeEditor(CUIRect View)
if(UI()->CheckActiveItem(pID))
{
CurrentTime = pEnvelope->m_vPoints[i].m_Time;
CurrentValue = pEnvelope->m_vPoints[i].m_aValues[c];
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(m_SelectedQuadEnvelope == m_SelectedEnvelope && m_SelectedEnvelopePoint == (int)i)
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<int>(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<int>(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(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);
}
// In-Tangent handle
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);
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<int>(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);
}
}
}
}
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");
}
}
}

View file

@ -45,17 +45,50 @@ enum
class CEnvelope
{
public:
class CEnvelopePointAccess : public IEnvelopePointAccess
{
std::vector<CEnvPoint_runtime> *m_pvPoints;
public:
CEnvelopePointAccess(std::vector<CEnvPoint_runtime> *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<CEnvPoint> m_vPoints;
public:
std::vector<CEnvPoint_runtime> 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<int>(Channels, 1, CEnvPoint::MAX_CHANNELS);
}
};

View file

@ -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<CDataFileWriterFinishJob> pWriterFinishJob = std::make_shared<CDataFileWriterFinishJob>(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() &&

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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")