This page out of date! I'll have to redo the tutorial for physics. The abstractions have changed in Bling 3, for the better mostly.

The code for the UI physics tutorial is located in the PhysicsTutorial project of the distribution.

Bling's support for physics is based on solving constraints that "project" physical property values. What this means is that you write a bunch of constraints (expressed via assignment) that directly move positions, change rotations, and so on. These constraints are installed on a world simulation object that executes them on every time step in the simulation. We introduce Bling UI physics concepts through examples in the physics tutorial. The examples include:
  • Sliders.cs introduces the UI physics concepts of blocks and physical properties, and demonstrates how UI physics can be applied to existing WPF widgets.
  • Chain.cs demonstrates how springs can be expressed in Bling to form chains, and integrates UI physics with Bling's pixel shader support.
  • Bezier.cs demonstrates more spring constraints and pixel shader integration.
  • ShapeIntro.cs demonstrates how to use higher-level particle and shape support for easing the definition of physical bodies.
  • Slinky.cs demonstrates how mouse input is added, and implements a slinky for a cool effect.
These examples can be run by executing the PhysicsTutorial project. A video of the examples being run is on YouTube.

Sliders.cs

Physics computations are aided in the notion of a physics block of UI objects. A block consists of bodies that wrap the blocks' UI objects and hold physical properties for the UI objects. A physical property is analogous to a dependency property except that it can be constrained where constraints are solved via integration in a physics engine. Finally, a world contains multiple blocks and manages the blocks during the physical simulation. The world object in turn can be wrapped up in a physics canvas that can be extended by the programmer:
  public class Sliders : PhysicsCanvas {
    public Sliders() : base("Sliders", 10) { }
    protected override Action Init(CanvasBl MainCanvas, World World) {

This code creates a class that extends PhysicsCanvas. The arguments of the base constructor are the name of the simulation and the time step (in this case 10 milliseconds). All of the interesting code will go in our definition for Init, where we first create a physics block of 20 sliders:
  PointBl P = new PointBl(0, 10);
  PlainBlock<Slider, SliderBl> block = new PlainBlock<Slider, SliderBl>(World, 20, i => {
  var slider = new SliderBl(MainCanvas) {
    Minimum = 0, Maximum = 1, Value = 0,
    Width = MainCanvas.Width, LeftTop = P,
  };
  slider.Background = i.SelectColor() * slider.Value;
  P = slider.LeftBottom;
  return slider;
}) { Name = "Sliders", };

We use PlainBlock for now because we don't require any extensions (in ShapeIntro.cs we will build a custom block). To create a plain physics block, we have to decide what will go in the block, which in this example are sliders. The Bling type of the underlying type must then also be specified (SliderBl). A plain physics block instance is created by specifying its containing world, the number of bodies that it will contain (20), and a function for creating each of the underlying UI objects. The rest is very standard: i.SelectColor() is used to give the slider a color based on its index, which is then multiplied by the slider's own value so that the color is invisible when the slider thumb is to the left and completely visible when the slider thumb is to the right. Next, we define a physical Value property for each block and define an accessor function
var ValueProperty = new PhysicsBlock.Property<double>(block, "Value");
Func<PlainBlock<Slider, SliderBl>.Body, DoubleBl> Value = 
  (body) => body.GetProperty(ValueProperty);

A property is created by specifying its parent, block, and type as a type parameter. The property is then accessed on a body using the GetProperty method, which we wrap in a Value function so Value(body) will access the property. Skipping ahead past the definition of a Strength slider and Mix button, the important physics logic happens within a ForAll statement:
block.ForAll(body => {
  Value(body).Link[!body.At.IsMouseOver] = body.At.Value;
  Value(body).Step = (now, old) => now.VerletStep(0.02, old);
  for (int i = 0; i < 3; i++) {
    Value(body).Relax[.5 * Strength, body.Index > 0] = Value(body).Max(Value(body.Prev[1]));
    Value(body).Relax[.5 * Strength, body.Index < block.Count - 1] = 
      Value(body).Min(Value(body.Next[1]));
  }
  Value(body).Old = Value(body).Old.Restitution(Value(body), 0, 1);
  Value(body).Bind = Value(body).Clamp(0, 1);
});
The body variable is bound to every body of the block, but in an abstract way: the loop is only called once and looping will only occur in the code generated to implement the constraints. Lots of things going on in body of the ForAll:
  • The first Link statement connects the physical Value properties of the bodies to the sliders' UI Value properties. As long as the mouse is not over the slider (!body.At.IsMouseOver), the physical value property will be copied to the slider's UI value property every 10 milliseconds, allowing the result of the physical simulation to be reflected in the UI. On the other hand, when the mouse is over the slider, the opposite happens: the slider's UI value property will be copied to the physical value property, allowing user input to be added to the simulation!
  • The next Step statement adds momentum to the value physical property using Verlet integration. Step will create an old version of the physical property, that can then be used to approximate the value's velocity using Verlet. Friction is simulated through a loss on each step, which in this case is 2%.
  • Two Relax statements are a pair of constraints that constraint the sliders thumbs to be in order. The first constraint ensures that the current slider does not fall behind the previous one, while the second constraint ensures that the slider does not pass the next one. Relax takes two arguments: the strength of the constraint and a guard that determines when the constraint is active. In this case, the strength of the constraint must be 50% or less as the constraint can affect two properties. The constraint is further weakened by the value of the strength slider, which can be changed by the user dynamically. A guard ensures that edge cases (the first and last sliders) are handled properly.
  • The last two statements add restitution to the physical values so the sliders bounce when they hit the edge, and clamp the physical values so they don't go off the edge.
Finally, we create a mix button that will set the slider physical property values to a random number when the button is pressed:
DoubleBl random = new Random();
Action<int> setValue0 = DoubleBl.Assign<int>(i => Value(block[i]), i => random);
Mix.Click = () => {
  for (int i = 0; i < block.Count; i++) setValue0(i);
};
The first statement creates a random double Bling value via an implicit coercion from Random to DoubleBl; the value will return a random double value whenever evaluated. The second statement creates an assignment of a specified body's physical value to random. The assignment is compiled via the DLR and will be executed very quickly when called. Finally, a click event handler is installed on the Mix button to call the assignment on every body when the butt on is pressed, hence giving all physical values a random value.

Chain.cs

In Chain.cs, we create a plain block of thumbs rather than sliders:
PlainBlock<Thumb, ThumbBl> block = new PlainBlock<Thumb, ThumbBl>(World, 9, i => {
        return new ThumbBl(MainCanvas) {
    CenterPosition = { Now = new PointBl(500, 500) * r },
    CanDrag = true, Background = i.SelectColor(), ZIndex = 2,
  };
}) { Name = "Chain", };
...and add a line between each thumb so that they resemble a chain:
for (int i = 0; i < block.Count - 1; i++) {
  new LineBl(MainCanvas) 
  { Start = block[i].At, ZIndex = 1, End = block[i + 1].At, Stroke = { Thickness = 3 }, };
}
As with Slider.cs, we create a physical property, this time a point to represent the thumb's position in the physical simulation:
var PositionProperty = new PhysicsBlock.Property<Point>(block, "Position");
Func<PlainBlock<Thumb, ThumbBl>.Body, PointBl> Position = 
  (body) => body.GetProperty(PositionProperty);
Next, the constraints for the bodies of the block:
block.ForAll(body => { 
  Position(body).Init = body.At.CenterPosition;
  Position(body).Link[!body.At.IsDragging] = body.At.CenterPosition;
  Position(body).Step = (now, old) => now.VerletStep(0.01, old);
  {
    Position(body).Relax[.5d * Strength.Value, body.Index > 0] = 
      Position(body).Spring(Position(body.Prev[1]), Length.Value);
    Position(body).Relax[.5d * Strength.Value, body.Index < block.Count - 1] = 
      Position(body).Spring(Position(body.Next[1]), Length.Value);
   }
   Position(body).Old = 
     Position(body).Old.Restitution(Position(body), new Point(0, 0), MainCanvas.Size);
   Position(body).Bind = Position(body).Clamp(new Point(0, 0), MainCanvas.Size);
});
In contrast to Sliders, the points are initialized according to the thumb's current UI position (body.At.CenterPosition) and its link with the UI position is managed by whether or not the thumb is being dragged, which means the thumb is being manipulated by the user. The two Relax constraints implement a "spring" that holds links of the chain together. Springs are simple mathematical functions in Bling, while the stiffness of the spring is managed by the strength of the constrained, which in this case can be weakened by the Strength slider.

Finally, the link ensures that the physical position properties are copied back to the UI thumb positions when the thumb isn't being dragged. These UI positions can then be used in pixel shader effects, so that physics is indirectly influencing the parameters of the effect. In this example, we add the metaballs effect from the metaball shader example.

Bezier.cs

In the Bezier curve example, we create another block of thumbs and bind them to a Bezier curve shape. A shader effect is then fixed to this Bezier shape, so that the physics can influence a genie like effect on the image.

ShapeIntro.cs

So far, we've been creating all physical properties and constraints ourselves, but Bling also supports high-level block extensions that provide for pre-defined properties and constraints. Two of these extensions are especially important to most physics simulations: ParticleBody which adds support for bodies that are defined by multiple particles each with their own position, and ShapeBody which computes center positions and rotations from particle positions and then uses these to re-adjust the particle positions to a common position and rotation. To use an extension, we have to create a custom physics block to implement Extension nested Is interfaces and instantiate/install Impl classes. We define a CustomBlock of images in ShapeIntro.cs as follows:
public class CustomBlock : PhysicsBlock<Image, ImageBl, CustomBlock>, 
  ParticleBody.Is<CustomBlock>, ShapeBody.Is<CustomBlock> {
  public CustomBlock(World World,int Count,Func<int,Image> MakeT):base(World,Count,MakeT) {
    new ParticleBody.Impl<CustomBlock>(this);
    new ShapeBody.Impl<CustomBlock>(this);
  }
}
By convention, all Extension classes contain an Is interface and an Impl class. The Is interface brands the block as implementing the extension, while the Impl class implements the extension's the actual behavior. Note that by convention, the last type parameter of a PhysicsBlock is the class being defined (hack to make extension methods work correctly). We instantiate the block mostly as normal:
CustomBlock block = new CustomBlock(World, Covers.Length, (idx) => {
  return new ImageBl(MainCanvas) {
    Source = Covers[idx],
    EnableCenterRotation = true,
  };
}) { Name = "ShapeIntro", };
Note that an EnableCenterRotation property is set to true. This is needed so that rotation can directly be specified on the image rather than going through a render transform. This needs to be set true for the shape extension to be able to influence the image's UI rotation property. We also define a second block of thumbs just to illustrate and manipulate the body's particles.
PlainBlock<Thumb,ThumbBl> Thumbs=new PlainBlock<Thumb,ThumbBl>(World,block.Count*4,(idx)=>{
  var j = idx % 4;
  return new ThumbBl(MainCanvas) {
    ZIndex = 2, Background = j.SelectColor(), CanDrag = true,
   };
}) { Name = "Thumbs", };
Next, we have to set the shape of the images in block, a convenience BindRect method is provided for rectangular shapes:
block.ShapePoints().BindRect(Covers[0].Size);
Finally, the constraints:
block.CustomInit = (block0) => {
  block0.ForAll(body => {
    body.ConnectShape();
    body.Particles().ForAll(particle => {
      var actual = body.Index * 4 + particle.Index;
      PointBl r = new Random();
      particle.Position.Init = (new PointBl(1000, 1000) * r);
      particle.Position.Link[!Thumbs[actual].At.IsDragging] =
        Thumbs[actual].At.CenterPosition;
      particle.Position.Old = 
   particle.Position.Old.Restitution(particle.Position,new Point(0,0),MainCanvas.Size);
      particle.Position = particle.Position.Clamp(new PointBl(0, 0), MainCanvas.Size);
    });
  });
};
Notice that we now have to wrap our constraints in the CustomInit property of the block to ensure that the constraints are added after the constraints of the extensions. The first statement in the outer ForAll, a call to ConnectShape, links the computed center and rotation physical properties to position and rotation UI properties so that the body moves according to shape computations. Particles() is a sub-block in body, and a nested ForAll statement allows us to express constraints on each property in the sub-block, which in this case is a Position physical property. The constraints on the position property is simply initialization, linking to the thumbs, restitution, and clamping.

Slinky.cs

Of course, moving bodies through thumbs isn't great, instead we'd like to grab the bodies directly with our mouse! This is what the MouseAdapter extension gives us. For slinky, we define another custom block of images:
public class CustomBlock : PhysicsBlock<Image, ImageBl, CustomBlock>,
  ParticleBody.Is<CustomBlock>,ShapeBody.Is<CustomBlock>,MouseAdapter.Is<CustomBlock,Image>{
  public CustomBlock(World World,int Count,Func<int,Image> MakeT):base(World,Count,MakeT) {
    new ParticleBody.Impl<CustomBlock>(this) { Loss = .2 };
    new ShapeBody.Impl<CustomBlock>(this);
    new MouseAdapter.Impl<CustomBlock,Image>(this);
  }
}
This is like the previous custom block, except we added the MouseAdapter extension to the mix.
CustomBlock block = new CustomBlock(World, 30, (idx) => {
  return new ImageBl(MainCanvas) {
    Source = Covers[(idx/5) % Covers.Length],
    EnableCenterRotation = true,
    Size = Covers[0].Size * .5,
  };
}) { Name = "Slinky", };
block.ShapePoints().BindRect(Covers[0].Size * .5);
No extra code is needed to deal with input, this is handled transparently by MouseAdapter. Now for the constraints:
block.CustomInit = (block0) => {
  block0.ForAll(body => {
    body.ConnectShape();
    body.Particles().ForAll(particle => {
      {
        var b = (particle.Index % 2) == (body.Index % 2);
        particle.Position.Relax[.5*Strength.Value, body.Index > 0 & b] =
    particle.Position.Spring(body.Prev[1].Particles(particle.Index).Position,Length.Value);
        particle.Position.Relax[.5*Strength.Value, body.Index < block.Count - 1 & b] =
    particle.Position.Spring(body.Next[1].Particles(particle.Index).Position,Length.Value);
      }
      particle.Position.Init = (new PointBl(1000, 1000) * new Random());
      particle.Position.Old = 
        particle.Position.Old.Restitution(particle.Position,new Point(0,0),MainCanvas.Size);
    particle.Position = particle.Position.Clamp(new PointBl(0, 0), MainCanvas.Size);
    });
  });
};
Nothing new here, except the constraints are a bit more interesting as springs are added between alternating two points on each element, creating a nice slinky effect.

Last edited Jul 20, 2009 at 5:52 AM by mcdirmid, version 25

Comments

No comments yet.