Skip to content

Commit

Permalink
[Animation] Improve adaptive sampling for splines
Browse files Browse the repository at this point in the history
Makes the adaptive sampling scheme for subdividing
spline segments more robust against control points
that are almost coinciding. Previously, coinciding
control points would result in infinite recursion
in some cases.
  • Loading branch information
hyazinthh committed Feb 5, 2025
1 parent bc58750 commit 6d781ec
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 22 deletions.
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- Fixed multisampled raw download
- [Animation] Improved adaptive sampling scheme for splines to avoid infinite recursion when control points are coinciding

### 5.5.2
- updated Adaptify.Core to 1.3.0 (using local, new style adaptify)
Expand Down
7 changes: 7 additions & 0 deletions src/Aardvark.Media.sln
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Aardvark.UI.Screenshotr", "
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "04 - Screenshotr", "Scratch\04 - Screenshotr\04 - Screenshotr.fsproj", "{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "32 - Splines", "Scratch\32 - Splines\32 - Splines.fsproj", "{B315793A-9E8F-4472-8DA4-58E507BFB2E3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -418,6 +420,10 @@ Global
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}.Release|Any CPU.Build.0 = Release|Any CPU
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -487,6 +493,7 @@ Global
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8} = {DAC89FC7-17D3-467D-929D-781A88DA5324}
{24D7C14A-3337-494B-8D72-4A2104D4956D} = {5DAFA99B-848D-4185-B4C1-287119815657}
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
{B315793A-9E8F-4472-8DA4-58E507BFB2E3} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B7FCCF28-D562-4E8F-86A7-2310B38A1016}
Expand Down
62 changes: 40 additions & 22 deletions src/Aardvark.UI.Primitives/Animation/Primitives/Splines.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ open Aardvark.Base
[<AutoOpen>]
module AnimationSplinePrimitives =

// TODO (Breaking): Rename user provided epsilon to errorTolerance
module Splines =

/// Epsilon value to compare difference between float values.
[<Literal>]
let Epsilon = 1e-8

/// Minimum error tolerance for subdividing spline segments.
[<Literal>]
let MinErrorTolerance = 1e-5

[<Struct>]
type private Segment<'T> =
{
mutable MinT : float
Expand All @@ -19,6 +29,7 @@ module AnimationSplinePrimitives =
/// Represents a spline segment, parameterized by normalized arc length.
/// The accuracy of the parameterization depends on the given epsilon, where values closer to zero result in higher accuracy.
type Spline<'T>(distance : 'T -> 'T -> float, evaluate : float -> 'T, epsilon : float) =
let errorTolerance = max epsilon MinErrorTolerance

let full =
let p0 = evaluate 0.0
Expand All @@ -33,27 +44,36 @@ module AnimationSplinePrimitives =
let p = evaluate t
let a = { s with MaxT = t; End = p; Length = distance s.Start p }
let b = { s with MinT = t; Start = p; Length = distance p s.End }
[a; b]
struct (a, b)

let subdivide (segment : Segment<'T>) =
let rec inner (accum : Segment<'T> list) (segments : Segment<'T> list) =
let rec inner (result : Segment<'T> list) (segments : Segment<'T> list) =
match segments with
| [] -> accum
| s::st ->
let halves = half s
let quarters = halves |> List.collect half
let quarterLength = s.Length * 0.25

let isQuarterValid (x : Segment<'T>) =
Fun.ApproximateEquals(x.Length / quarterLength, 1.0, epsilon)

if (quarters |> List.forall isQuarterValid) then
inner (s :: accum) st // avoid O(n) append, reverse when finished
| [] ->
result |> List.rev |> Array.ofList // Avoid O(n) append, reverse when finished

// Do not subdivide if length is below epsilon
| s::rest when s.Length < Epsilon ->
inner (s :: result) rest

| s::rest ->
let struct (a, b) = half s

// Subdivide if ratio between sum of halves and full segment exceeds tolerance
// Also force an initial subdivision
let isWithinErrorTolerance() =
if result.IsEmpty && rest.IsEmpty then false
else
let errorRatio = (a.Length + b.Length) / s.Length
Fun.ApproximateEquals(errorRatio, 1.0, errorTolerance)

if a.Length < Epsilon || b.Length < Epsilon || isWithinErrorTolerance() then
inner (s :: result) rest
else
inner accum (halves @ st)
inner result (a :: b :: rest)

if isFinite epsilon then
[ segment ] |> inner [] |> List.rev |> Array.ofList
if isFinite errorTolerance then
[ segment ] |> inner []
else
[| segment |]

Expand All @@ -66,7 +86,7 @@ module AnimationSplinePrimitives =
Length = s.Length }
)

//// Sum and normalize
// Sum and normalize
let n = s.Length
let mutable sum = KahanSum.Zero

Expand All @@ -90,7 +110,7 @@ module AnimationSplinePrimitives =
elif s > 1.0 then segments.Length - 1
else
segments |> Array.binarySearch (fun segment ->
if s < segment.Start then -1 elif s > segment.End then 1 else 0
if s < segment.Start then -1 elif s > segment.End then 1 else 0
) |> ValueOption.get

let t = Fun.InvLerp(s, segments.[i].Start, segments.[i].End)
Expand Down Expand Up @@ -126,7 +146,6 @@ module AnimationSplinePrimitives =

Spline(distance, evaluate, epsilon)


if Array.isEmpty points then
Array.empty
else
Expand All @@ -141,7 +160,7 @@ module AnimationSplinePrimitives =

for i = 1 to points.Length - 1 do
let d = sqrt (distance pj.[n - 1] points.[i])
if d.ApproximateEquals 0.0 then
if d.IsTiny Epsilon then
Log.warn "[Animation] Ignoring duplicate control point in spline"
else
pj.[n] <- points.[i]
Expand Down Expand Up @@ -177,10 +196,9 @@ module AnimationSplinePrimitives =
/// The animations are scaled according to the distance between the points. Coinciding points are ignored.
/// The accuracy of the parameterization depends on the given epsilon, where values closer to zero result in higher accuracy.
let inline smoothPath' (distance : ^Value -> ^Value -> float) (epsilon : float) (points : ^Value seq) : IAnimation<'Model, ^Value>[] =

let points = Array.ofSeq points
let spline = points |> Splines.catmullRom distance epsilon
let maxLength = spline |> Array.map (fun s -> s.Length) |> Array.max
let maxLength = spline.MaxValue _.Length

spline |> Array.map (fun s ->
let duration = s.Length / maxLength
Expand Down
30 changes: 30 additions & 0 deletions src/Scratch/32 - Splines/32 - Splines.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<DisableImplicitFSharpCoreReference>True</DisableImplicitFSharpCoreReference>
<AssemblyName>$(MSBuildProjectName.Replace(" ", "_"))</AssemblyName>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\..\bin\Debug\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\..\bin\Release\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Model.fs" />
<Compile Include="App.fs" />
<Compile Include="Program.fs" />
<None Include="App.config" />
<None Include="paket.references" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Aardvark.Service\Aardvark.Service.fsproj" />
<ProjectReference Include="..\..\Aardvark.UI.Primitives\Aardvark.UI.Primitives.fsproj" />
<ProjectReference Include="..\..\Aardvark.UI\Aardvark.UI.fsproj" />
</ItemGroup>
<Import Project="..\..\..\.paket\Paket.Restore.targets" />
</Project>
6 changes: 6 additions & 0 deletions src/Scratch/32 - Splines/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
130 changes: 130 additions & 0 deletions src/Scratch/32 - Splines/App.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
module SplinesTest.App

open Aardvark.Base
open Aardvark.Rendering
open Aardvark.Application
open Aardvark.UI
open Aardvark.UI.Animation
open FSharp.Data.Adaptive
open SplinesTest.Model

[<AutoOpen>]
module PointShaders =
open FShade

type UniformScope with
member x.PointSize : float = uniform?PointSize

module Semantic =
let PointSize = Sym.ofString "PointSize"

module Shader =
type PointVertex =
{
[<Position>] pos : V4d
[<TexCoord; Interpolation(InterpolationMode.Sample)>] tc : V2d
}

let pointTransform (v: PointVertex) =
vertex {
return V4d(v.pos.XY * 2.0 - 1.0, 0.0, 1.0)
}

let camera =
let view = CameraView.look V3d.Zero V3d.YAxis V3d.ZAxis
let frustum = Frustum.perspective 90.0 0.1 100.0 1.0
AVal.constant <| Camera.create view frustum

let addTolerance (delta: float) (model: Model) =
{ model with ErrorTolerance = clamp Splines.MinErrorTolerance 0.2 (model.ErrorTolerance + delta) }

let update (model: Model) (msg: Message) =
match msg with
| Add p ->
{ model with Points = Array.append model.Points [| V2d(p.X, 1.0 - p.Y) |] }

| OnKeyDown Keys.Delete ->
{ model with Points = [||] }

| OnKeyDown Keys.Back ->
let n = max 0 (model.Points.Length - 1)
{ model with Points = model.Points |> Array.take n}

| OnKeyDown Keys.OemPlus ->
model |> addTolerance 0.001

| OnKeyDown Keys.OemMinus ->
model |> addTolerance -0.001

| OnWheel d->
model |> addTolerance (0.0001 * signum d.Y)

| _ ->
model

let view (model: AdaptiveModel) =
let p0 = RenderPass.main
let p1 = p0 |> RenderPass.after "p1" RenderPassOrder.Arbitrary
let p2 = p1 |> RenderPass.after "p1" RenderPassOrder.Arbitrary

let renderPoints (pass: RenderPass) (color: C4f) (size: float) (points: aval<V2d[]>) =
let points =
points |> AVal.map(Seq.map v3f >> Seq.toArray)

Sg.draw IndexedGeometryMode.PointList
|> Sg.vertexAttribute DefaultSemantic.Positions points
|> Sg.uniform' Semantic.PointSize size
|> Sg.shader {
do! Shader.pointTransform
do! DefaultSurfaces.pointSprite
do! DefaultSurfaces.pointSpriteFragment
do! DefaultSurfaces.constantColor color
}
|> Sg.pass pass

let sg =
let splinePoints =
model.Splines |> AVal.map (fun s ->
s |> Array.collect (fun s ->
let n = int <| s.Length * 512.0
Array.init n (fun i ->
s.Evaluate <| float i / float (n - 1)
)
)
)
|> renderPoints p0 C4f.DarkRed 4.0

let controlPoints = model.Points |> renderPoints p1 C4f.AliceBlue 12.0
let samplePoints = model.Samples |> renderPoints p2 C4f.VRVisGreen 8.0

Sg.ofList [ controlPoints; samplePoints; splinePoints ]

body [
style "width: 100%; height: 100%; border: 0; padding: 0; margin: 0; overflow: hidden"
] [
renderControl camera [
style "width: 100%; height: 100%; border: 0; padding: 0; margin: 0; overflow: hidden"
onMouseClickRel (fun _ p -> Add p)
onKeyDown OnKeyDown
onWheel OnWheel
] sg

div [style "position: absolute; left: 10px; top: 10px; color: white; pointer-events: none; font-family: monospace; font-size: larger;"] [
model.ErrorTolerance
|> AVal.map (fun t -> $"Error tolerance: %.5f{t}")
|> Incremental.text
]
]

let app : App<_,_,_> =
{
unpersist = Unpersist.instance
threads = fun _ -> ThreadPool.empty
initial =
{
Points = [||]
ErrorTolerance = 0.01
}
update = update
view = view
}
41 changes: 41 additions & 0 deletions src/Scratch/32 - Splines/AssemblyInfo.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace _01___Inc.AssemblyInfo

open System.Reflection
open System.Runtime.CompilerServices
open System.Runtime.InteropServices

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[<assembly: AssemblyTitle("01 - Inc")>]
[<assembly: AssemblyDescription("")>]
[<assembly: AssemblyConfiguration("")>]
[<assembly: AssemblyCompany("")>]
[<assembly: AssemblyProduct("01 - Inc")>]
[<assembly: AssemblyCopyright("Copyright © 2018")>]
[<assembly: AssemblyTrademark("")>]
[<assembly: AssemblyCulture("")>]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[<assembly: ComVisible(false)>]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[<assembly: Guid("206c17a2-a3a1-4191-8ca3-da92197bb73b")>]

// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [<assembly: AssemblyVersion("1.0.*")>]
[<assembly: AssemblyVersion("1.0.0.0")>]
[<assembly: AssemblyFileVersion("1.0.0.0")>]

do
()
Loading

0 comments on commit 6d781ec

Please sign in to comment.