Skip to content

Commit 6d781ec

Browse files
committed
[Animation] Improve adaptive sampling for splines
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.
1 parent bc58750 commit 6d781ec

File tree

10 files changed

+342
-22
lines changed

10 files changed

+342
-22
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
- Fixed multisampled raw download
2+
- [Animation] Improved adaptive sampling scheme for splines to avoid infinite recursion when control points are coinciding
23

34
### 5.5.2
45
- updated Adaptify.Core to 1.3.0 (using local, new style adaptify)

src/Aardvark.Media.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Aardvark.UI.Screenshotr", "
156156
EndProject
157157
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "04 - Screenshotr", "Scratch\04 - Screenshotr\04 - Screenshotr.fsproj", "{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}"
158158
EndProject
159+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "32 - Splines", "Scratch\32 - Splines\32 - Splines.fsproj", "{B315793A-9E8F-4472-8DA4-58E507BFB2E3}"
160+
EndProject
159161
Global
160162
GlobalSection(SolutionConfigurationPlatforms) = preSolution
161163
Debug|Any CPU = Debug|Any CPU
@@ -418,6 +420,10 @@ Global
418420
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}.Debug|Any CPU.Build.0 = Debug|Any CPU
419421
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}.Release|Any CPU.ActiveCfg = Release|Any CPU
420422
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B}.Release|Any CPU.Build.0 = Release|Any CPU
423+
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
424+
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
425+
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
426+
{B315793A-9E8F-4472-8DA4-58E507BFB2E3}.Release|Any CPU.Build.0 = Release|Any CPU
421427
EndGlobalSection
422428
GlobalSection(SolutionProperties) = preSolution
423429
HideSolutionNode = FALSE
@@ -487,6 +493,7 @@ Global
487493
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8} = {DAC89FC7-17D3-467D-929D-781A88DA5324}
488494
{24D7C14A-3337-494B-8D72-4A2104D4956D} = {5DAFA99B-848D-4185-B4C1-287119815657}
489495
{F54A0F8D-DBF7-4483-B47E-DFBB3273027B} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
496+
{B315793A-9E8F-4472-8DA4-58E507BFB2E3} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
490497
EndGlobalSection
491498
GlobalSection(ExtensibilityGlobals) = postSolution
492499
SolutionGuid = {B7FCCF28-D562-4E8F-86A7-2310B38A1016}

src/Aardvark.UI.Primitives/Animation/Primitives/Splines.fs

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@ open Aardvark.Base
55
[<AutoOpen>]
66
module AnimationSplinePrimitives =
77

8+
// TODO (Breaking): Rename user provided epsilon to errorTolerance
89
module Splines =
910

11+
/// Epsilon value to compare difference between float values.
12+
[<Literal>]
13+
let Epsilon = 1e-8
14+
15+
/// Minimum error tolerance for subdividing spline segments.
16+
[<Literal>]
17+
let MinErrorTolerance = 1e-5
18+
19+
[<Struct>]
1020
type private Segment<'T> =
1121
{
1222
mutable MinT : float
@@ -19,6 +29,7 @@ module AnimationSplinePrimitives =
1929
/// Represents a spline segment, parameterized by normalized arc length.
2030
/// The accuracy of the parameterization depends on the given epsilon, where values closer to zero result in higher accuracy.
2131
type Spline<'T>(distance : 'T -> 'T -> float, evaluate : float -> 'T, epsilon : float) =
32+
let errorTolerance = max epsilon MinErrorTolerance
2233

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

3849
let subdivide (segment : Segment<'T>) =
39-
let rec inner (accum : Segment<'T> list) (segments : Segment<'T> list) =
50+
let rec inner (result : Segment<'T> list) (segments : Segment<'T> list) =
4051
match segments with
41-
| [] -> accum
42-
| s::st ->
43-
let halves = half s
44-
let quarters = halves |> List.collect half
45-
let quarterLength = s.Length * 0.25
46-
47-
let isQuarterValid (x : Segment<'T>) =
48-
Fun.ApproximateEquals(x.Length / quarterLength, 1.0, epsilon)
49-
50-
if (quarters |> List.forall isQuarterValid) then
51-
inner (s :: accum) st // avoid O(n) append, reverse when finished
52+
| [] ->
53+
result |> List.rev |> Array.ofList // Avoid O(n) append, reverse when finished
54+
55+
// Do not subdivide if length is below epsilon
56+
| s::rest when s.Length < Epsilon ->
57+
inner (s :: result) rest
58+
59+
| s::rest ->
60+
let struct (a, b) = half s
61+
62+
// Subdivide if ratio between sum of halves and full segment exceeds tolerance
63+
// Also force an initial subdivision
64+
let isWithinErrorTolerance() =
65+
if result.IsEmpty && rest.IsEmpty then false
66+
else
67+
let errorRatio = (a.Length + b.Length) / s.Length
68+
Fun.ApproximateEquals(errorRatio, 1.0, errorTolerance)
69+
70+
if a.Length < Epsilon || b.Length < Epsilon || isWithinErrorTolerance() then
71+
inner (s :: result) rest
5272
else
53-
inner accum (halves @ st)
73+
inner result (a :: b :: rest)
5474

55-
if isFinite epsilon then
56-
[ segment ] |> inner [] |> List.rev |> Array.ofList
75+
if isFinite errorTolerance then
76+
[ segment ] |> inner []
5777
else
5878
[| segment |]
5979

@@ -66,7 +86,7 @@ module AnimationSplinePrimitives =
6686
Length = s.Length }
6787
)
6888

69-
//// Sum and normalize
89+
// Sum and normalize
7090
let n = s.Length
7191
let mutable sum = KahanSum.Zero
7292

@@ -90,7 +110,7 @@ module AnimationSplinePrimitives =
90110
elif s > 1.0 then segments.Length - 1
91111
else
92112
segments |> Array.binarySearch (fun segment ->
93-
if s < segment.Start then -1 elif s > segment.End then 1 else 0
113+
if s < segment.Start then -1 elif s > segment.End then 1 else 0
94114
) |> ValueOption.get
95115

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

127147
Spline(distance, evaluate, epsilon)
128148

129-
130149
if Array.isEmpty points then
131150
Array.empty
132151
else
@@ -141,7 +160,7 @@ module AnimationSplinePrimitives =
141160

142161
for i = 1 to points.Length - 1 do
143162
let d = sqrt (distance pj.[n - 1] points.[i])
144-
if d.ApproximateEquals 0.0 then
163+
if d.IsTiny Epsilon then
145164
Log.warn "[Animation] Ignoring duplicate control point in spline"
146165
else
147166
pj.[n] <- points.[i]
@@ -177,10 +196,9 @@ module AnimationSplinePrimitives =
177196
/// The animations are scaled according to the distance between the points. Coinciding points are ignored.
178197
/// The accuracy of the parameterization depends on the given epsilon, where values closer to zero result in higher accuracy.
179198
let inline smoothPath' (distance : ^Value -> ^Value -> float) (epsilon : float) (points : ^Value seq) : IAnimation<'Model, ^Value>[] =
180-
181199
let points = Array.ofSeq points
182200
let spline = points |> Splines.catmullRom distance epsilon
183-
let maxLength = spline |> Array.map (fun s -> s.Length) |> Array.max
201+
let maxLength = spline.MaxValue _.Length
184202

185203
spline |> Array.map (fun s ->
186204
let duration = s.Length / maxLength
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<DisableImplicitFSharpCoreReference>True</DisableImplicitFSharpCoreReference>
7+
<AssemblyName>$(MSBuildProjectName.Replace(" ", "_"))</AssemblyName>
8+
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
9+
</PropertyGroup>
10+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
11+
<OutputPath>..\..\..\bin\Debug\</OutputPath>
12+
</PropertyGroup>
13+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
14+
<OutputPath>..\..\..\bin\Release\</OutputPath>
15+
</PropertyGroup>
16+
<ItemGroup>
17+
<Compile Include="AssemblyInfo.fs" />
18+
<Compile Include="Model.fs" />
19+
<Compile Include="App.fs" />
20+
<Compile Include="Program.fs" />
21+
<None Include="App.config" />
22+
<None Include="paket.references" />
23+
</ItemGroup>
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\Aardvark.Service\Aardvark.Service.fsproj" />
26+
<ProjectReference Include="..\..\Aardvark.UI.Primitives\Aardvark.UI.Primitives.fsproj" />
27+
<ProjectReference Include="..\..\Aardvark.UI\Aardvark.UI.fsproj" />
28+
</ItemGroup>
29+
<Import Project="..\..\..\.paket\Paket.Restore.targets" />
30+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<startup>
4+
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
5+
</startup>
6+
</configuration>

src/Scratch/32 - Splines/App.fs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
module SplinesTest.App
2+
3+
open Aardvark.Base
4+
open Aardvark.Rendering
5+
open Aardvark.Application
6+
open Aardvark.UI
7+
open Aardvark.UI.Animation
8+
open FSharp.Data.Adaptive
9+
open SplinesTest.Model
10+
11+
[<AutoOpen>]
12+
module PointShaders =
13+
open FShade
14+
15+
type UniformScope with
16+
member x.PointSize : float = uniform?PointSize
17+
18+
module Semantic =
19+
let PointSize = Sym.ofString "PointSize"
20+
21+
module Shader =
22+
type PointVertex =
23+
{
24+
[<Position>] pos : V4d
25+
[<TexCoord; Interpolation(InterpolationMode.Sample)>] tc : V2d
26+
}
27+
28+
let pointTransform (v: PointVertex) =
29+
vertex {
30+
return V4d(v.pos.XY * 2.0 - 1.0, 0.0, 1.0)
31+
}
32+
33+
let camera =
34+
let view = CameraView.look V3d.Zero V3d.YAxis V3d.ZAxis
35+
let frustum = Frustum.perspective 90.0 0.1 100.0 1.0
36+
AVal.constant <| Camera.create view frustum
37+
38+
let addTolerance (delta: float) (model: Model) =
39+
{ model with ErrorTolerance = clamp Splines.MinErrorTolerance 0.2 (model.ErrorTolerance + delta) }
40+
41+
let update (model: Model) (msg: Message) =
42+
match msg with
43+
| Add p ->
44+
{ model with Points = Array.append model.Points [| V2d(p.X, 1.0 - p.Y) |] }
45+
46+
| OnKeyDown Keys.Delete ->
47+
{ model with Points = [||] }
48+
49+
| OnKeyDown Keys.Back ->
50+
let n = max 0 (model.Points.Length - 1)
51+
{ model with Points = model.Points |> Array.take n}
52+
53+
| OnKeyDown Keys.OemPlus ->
54+
model |> addTolerance 0.001
55+
56+
| OnKeyDown Keys.OemMinus ->
57+
model |> addTolerance -0.001
58+
59+
| OnWheel d->
60+
model |> addTolerance (0.0001 * signum d.Y)
61+
62+
| _ ->
63+
model
64+
65+
let view (model: AdaptiveModel) =
66+
let p0 = RenderPass.main
67+
let p1 = p0 |> RenderPass.after "p1" RenderPassOrder.Arbitrary
68+
let p2 = p1 |> RenderPass.after "p1" RenderPassOrder.Arbitrary
69+
70+
let renderPoints (pass: RenderPass) (color: C4f) (size: float) (points: aval<V2d[]>) =
71+
let points =
72+
points |> AVal.map(Seq.map v3f >> Seq.toArray)
73+
74+
Sg.draw IndexedGeometryMode.PointList
75+
|> Sg.vertexAttribute DefaultSemantic.Positions points
76+
|> Sg.uniform' Semantic.PointSize size
77+
|> Sg.shader {
78+
do! Shader.pointTransform
79+
do! DefaultSurfaces.pointSprite
80+
do! DefaultSurfaces.pointSpriteFragment
81+
do! DefaultSurfaces.constantColor color
82+
}
83+
|> Sg.pass pass
84+
85+
let sg =
86+
let splinePoints =
87+
model.Splines |> AVal.map (fun s ->
88+
s |> Array.collect (fun s ->
89+
let n = int <| s.Length * 512.0
90+
Array.init n (fun i ->
91+
s.Evaluate <| float i / float (n - 1)
92+
)
93+
)
94+
)
95+
|> renderPoints p0 C4f.DarkRed 4.0
96+
97+
let controlPoints = model.Points |> renderPoints p1 C4f.AliceBlue 12.0
98+
let samplePoints = model.Samples |> renderPoints p2 C4f.VRVisGreen 8.0
99+
100+
Sg.ofList [ controlPoints; samplePoints; splinePoints ]
101+
102+
body [
103+
style "width: 100%; height: 100%; border: 0; padding: 0; margin: 0; overflow: hidden"
104+
] [
105+
renderControl camera [
106+
style "width: 100%; height: 100%; border: 0; padding: 0; margin: 0; overflow: hidden"
107+
onMouseClickRel (fun _ p -> Add p)
108+
onKeyDown OnKeyDown
109+
onWheel OnWheel
110+
] sg
111+
112+
div [style "position: absolute; left: 10px; top: 10px; color: white; pointer-events: none; font-family: monospace; font-size: larger;"] [
113+
model.ErrorTolerance
114+
|> AVal.map (fun t -> $"Error tolerance: %.5f{t}")
115+
|> Incremental.text
116+
]
117+
]
118+
119+
let app : App<_,_,_> =
120+
{
121+
unpersist = Unpersist.instance
122+
threads = fun _ -> ThreadPool.empty
123+
initial =
124+
{
125+
Points = [||]
126+
ErrorTolerance = 0.01
127+
}
128+
update = update
129+
view = view
130+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace _01___Inc.AssemblyInfo
2+
3+
open System.Reflection
4+
open System.Runtime.CompilerServices
5+
open System.Runtime.InteropServices
6+
7+
// General Information about an assembly is controlled through the following
8+
// set of attributes. Change these attribute values to modify the information
9+
// associated with an assembly.
10+
[<assembly: AssemblyTitle("01 - Inc")>]
11+
[<assembly: AssemblyDescription("")>]
12+
[<assembly: AssemblyConfiguration("")>]
13+
[<assembly: AssemblyCompany("")>]
14+
[<assembly: AssemblyProduct("01 - Inc")>]
15+
[<assembly: AssemblyCopyright("Copyright © 2018")>]
16+
[<assembly: AssemblyTrademark("")>]
17+
[<assembly: AssemblyCulture("")>]
18+
19+
// Setting ComVisible to false makes the types in this assembly not visible
20+
// to COM components. If you need to access a type in this assembly from
21+
// COM, set the ComVisible attribute to true on that type.
22+
[<assembly: ComVisible(false)>]
23+
24+
// The following GUID is for the ID of the typelib if this project is exposed to COM
25+
[<assembly: Guid("206c17a2-a3a1-4191-8ca3-da92197bb73b")>]
26+
27+
// Version information for an assembly consists of the following four values:
28+
//
29+
// Major Version
30+
// Minor Version
31+
// Build Number
32+
// Revision
33+
//
34+
// You can specify all the values or you can default the Build and Revision Numbers
35+
// by using the '*' as shown below:
36+
// [<assembly: AssemblyVersion("1.0.*")>]
37+
[<assembly: AssemblyVersion("1.0.0.0")>]
38+
[<assembly: AssemblyFileVersion("1.0.0.0")>]
39+
40+
do
41+
()

0 commit comments

Comments
 (0)