Spline y Strange Attractor

Un Strange Attractor es un artilugio matemático, un sistema que evoluciona, empezamos con un punto inicial y calculamos el siguiente, y el siguiente, y el siguiente, paso a paso. El resultado de esta evolución son figuras hermosas llamadas Strange Attractor.

DeQuan Li Attractor

Vamos a desarrollarlo en UE4, será la excusa perfecta para estudiar como trabajar con Splines en el motor.

Splines en UE4

Trabajar con Splines en UE4 es realmente sencillo. Disponemos del componente USplineComponent.

UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "StrangeAttractor")
class USplineComponent* SplineComponent;

Vamos a crear un spline que aproxime la figura del Strange Attractor llamado DeQuan Li.

¿Cuáles son las ecuaciones que determinan, partiendo de un punto $P = (x, y, z)$, el siguiente punto?

Para el caso de DeQuan Li son las siguientes:

$$ \frac{dx}{dt} = A (y - x) + D x z  $$

$$ \frac{dy}{dt} = K x + F y - x z $$

$$ \frac{dz}{dt} = C z + x y - E x ^ 2 $$

Dónde $A, D, K, F, C, E$ son parámetros cuyos valores podemos variar para crear distintas figuras.

Para otros Stange Attractor son otras ecuaciones.

Obviamente $dt$ es un infinitesimal que no podemos implementar tal cuál. En vez de eso, lo aproximaremos por un valor muy pequeño:

$$ dt = 0.00001 $$

La figura que acompaña este post está generada con los siguientes valores:

$$ P_0 = (0.349, 0, -0.16) $$
$$ A = 40, C = 1.8333, D = 0.16, E = 0.65, K = 55, F = 20 $$

Dónde $P_o$ es el punto inicial.

En UE4 podemos encapsular todos los parámetros que definen el Strange Attractor en una estructura:

USTRUCT(BlueprintType)
struct FSplineParams {
  GENERATED_BODY()

  UPROPERTY(EditAnywhere)
  FVector StartPos;

  UPROPERTY(EditAnywhere)
  float A;
  UPROPERTY(EditAnywhere)
  float C;
  UPROPERTY(EditAnywhere)
  float D;
  UPROPERTY(EditAnywhere)
  float E;
  UPROPERTY(EditAnywhere)
  float K;
  UPROPERTY(EditAnywhere)
  float F;
};
Estructura que contiene los parámetros del Strange Attractor

Y en el constructor de nuestro actor:

Params.StartPos = FVector(0.349, 0, -0.16);
Params.A = 40.0f;
Params.C = 1.8333f;
Params.D = 0.16f;
Params.E = 0.65f;
Params.K = 55.0f;
Params.F = 20.0f;
Parámetros de ejemplo

Aquí el método que genera el Spline:

void ASSpline::BuildSpline()
{
  static float dt = 0.00001f;	
  static int NumPoints = 2000;

  SplineComponent->ClearSplinePoints();

  FVector LastPos = Params.StartPos;

  for (int i = 0; i < NumPoints; ++i) {
    FVector NewPos = LastPos;
    do {
      FVector PrevPos = NewPos;
      NewPos.X += (Params.A * (PrevPos.Y - PrevPos.X) + Params.D * PrevPos.X * PrevPos.Z) * dt;
      NewPos.Y += (Params.K * PrevPos.X + Params.F * PrevPos.Y - PrevPos.X * PrevPos.Z) * dt;
      NewPos.Z += (Params.C * PrevPos.Z + PrevPos.X * PrevPos.Y - Params.E * PrevPos.X * PrevPos.X) * dt;
    } while (FVector::Dist(LastPos, NewPos) < 50.0f);

    SplineComponent->AddSplinePoint(NewPos, ESplineCoordinateSpace::Local, true);
    LastPos = NewPos;
  }	
}
Construir el Spline

El bucle interior simplemente es para evitar que existan dos puntos consecutivos excesivamente juntos, como mínimo que el siguiente punto que añadamos al Spline tenga una separación de 50 unidades.

Como ves, la API de SplineComponent es trivial: con ClearSplinePoints borras todos los puntos previamente añadidos y con AddSplinePoint añades nuevos puntos.

En este caso en concreto, el método BuildSpline puede ser costoso. Para aliviar la carga, podemos aprovechar las capacidades multihilo que proporciona UE4:

void ASSpline::BeginPlay()
{
  Super::BeginPlay();
  Async(EAsyncExecution::TaskGraphMainThread, [&] {
    BuildSpline();
  });
}
Multihilo para BuildSpline

Creado el Spline la siguiente pregunta de rigor es: ¿qué podemos hacer con un Spline?

Crear un mesh a partir de un Spline

Podemos crear una geometría a partir de un Spline.

Spline Mesh

Para ello nos apoyamos de otro componente: USplineMeshComponent.

El componente SplineMeshComponent está limitado a Splines de dos puntos.

En concreto, toma un static mesh, un punto y tangente inicial y un punto y tangente final. Entonces distorsiona el mesh de forma que siga este spline de dos puntos.

¿Cómo podemos usar este componente para construir un spline completo de más de 2 puntos? Fácil, crearemos un componente usando los primeros dos puntos $P_0$, $P_1$, construiremos otro nuevo componente para $P_1$ y $P_2$, otro nuevo para $P_2$, $P_3$, y así hasta $P_{n-1}$, $P_n$.

La primera tentación sería crear los componentes con:

CreateDefaultSubobject<USplineMeshComponent>(TEXT("..."))

Sin embargo este método solo es posible usarlo en el constructor. Está pensado para componentes "fijos", en el sentido de que son componentes que no construiremos de manera dinámica a lo largo del tiempo de ejecución.

Pero precisamente esto es lo que necesitamos con SplineMeshComponent, necesitamos crearlo fuera del constructor y de manera dinámica.

Para ello tenemos a nuestra disposición la siguiente función:

USplineMeshComponent* SplineMesh = NewObject<USplineMeshComponent>(this);
SplineMesh->RegisterComponent();

Dicho esto, este es el método para construir el mesh:

void ASSpline::BuildMesh()
{
  for (int Index = 0; Index < SplineComponent->GetNumberOfSplinePoints() - 1; ++Index)
  {
    FVector StartPos, StartTangent;
    SplineComponent->GetLocationAndTangentAtSplinePoint(Index, StartPos, StartTangent, ESplineCoordinateSpace::Local);

    FVector EndPos, EndTangent;
    SplineComponent->GetLocationAndTangentAtSplinePoint(Index + 1, EndPos, EndTangent, ESplineCoordinateSpace::Local);

    USplineMeshComponent* SplineMeshComp = NewObject<USplineMeshComponent>(this);
    SplineMeshComp->RegisterComponent();

    SplineMeshComp->SetMobility(EComponentMobility::Movable);
    SplineMeshComp->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);

    SplineMeshComp->SetStaticMesh(StaticMesh);
    SplineMeshComp->SetMaterial(0, StaticMeshMaterial);

    SplineMeshComp->SetForwardAxis(ESplineMeshAxis::X);
    SplineMeshComp->SetStartAndEnd(StartPos, StartTangent, EndPos, EndTangent, true);
  }
}
Build Spline Meshes

Seguir un Spline

Además de construir un mesh, también podemos hacer que un componente siga un spline.

Para ilustrarlo, vamos a hacer que un NiagaraComponent siga el spline.

UCLASS()
class STANGEATTRACTORS_API ASSpline : public AActor
{
  GENERATED_BODY()
    
  UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "StrangeAttractor")
  class UNiagaraComponent* ParticleSystemComponent;
	
public:	
  ASSpline();
    
  // ...

private:
  float SplineStep;
  bool bSplineBuilt;
};
void ASSpline::BeginPlay()
{
  Super::BeginPlay();
  Async(EAsyncExecution::TaskGraphMainThread, [&] {
    BuildSpline();
    BuildMesh();
    SplineStep = 0.0f;
    bSplineBuilt = true;	
  });
}

void ASSpline::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);

  if (!bSplineBuilt)
  {
    return;
  }

  if (SplineStep > SplineComponent->GetSplineLength()) {
    SplineStep = 0.0f;
  }
	
  // SplineComponent->GetLocationAtTime(Time, CoordinateSpace, bUseConstantVelocity)
    
  FVector PSLocation = SplineComponent->GetLocationAtTime(SplineStep, ESplineCoordinateSpace::Local, true);
  ParticleSystemComponent->SetRelativeLocation(PSLocation);

  SplineStep += 0.01f * DeltaTime;
}
Seguir un Spline

La API es bastante sencilla. Toma como un parámetro un float llamado "Time" que va de 0 a SplineComponent->GetSplineLength() y devuelve la posición correspondiente en el Spline.

Referencias

En esta web encontrarás muchas definiciones de Strange Attractor y sus parámetro usados:

http://3d-meier.de/

Jorge Moreno Aguilera

Jorge Moreno Aguilera