December 2014

Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      










« Heading for San Francisco | Main | Creating a 3D viewer for our Apollonian service using iOS – Part 3 »

May 30, 2012

Creating a 3D viewer for our Apollonian service using WinRT – Part 2

In the previous post in this series, we saw the code for an initial, basic implementation of a 3D viewer for our Apollonian web-service developed for Windows 8 using WinRT. In this post, we extend that code to provide support for a few basic gestures, particularly swipe-spin, pinch-zoom and tap-pause.

To properly show the gestures in action, I recorded the app working inside the Windows 8 emulator (which in turn was running inside Windows 8 running inside a Parallels VM, so fairly far from “the metal”, as it were).

Here’s a quick video of the updated app in action:

Unable to display content. Adobe Flash is required.

Here’s the code for the main ApollonianRenderer.cs file:

using System;

using System.Diagnostics;

using System.Collections.Generic;

using System.Net.Http;

using System.Threading.Tasks;

using Windows.Data.Json;

using CommonDX;

using SharpDX;

using SharpDX.Direct3D;

using SharpDX.Direct3D11;

using SharpDX.DXGI;

using SharpDX.IO;

using Buffer = SharpDX.Direct3D11.Buffer;

using Device = SharpDX.Direct3D11.Device;

 

namespace ApollonianViewer

{

  // We allow the UI to register an event to be called

  // once a level has been loaded completely

 

  public delegate void RenderInitCompletedEventHandler(

    object sender, EventArgs e

  );

 

  public class ApollonianRenderer : Component

  {

    Device _device;

 

    // Our core geometry data

 

    BasicVertex[] _vertices;

    short[] _indices;

    private int _vertCount;

    private int _idxCount;

 

    // Our members to map this data for the GPU

 

    private InputLayout _layout;

    private Buffer[] _vertBufs;

    private int[] _vertStrides;

    private int[] _vertOffsets;

    private Buffer _idxBuf;

    private Buffer _constBuf;

    private int _instCount;

    private Stopwatch _clock;

    private VertexShader _vertShader;

    private PixelShader _pxlShader;

 

    // The current level we're rendering

 

    private int _level;

 

    // Our model transformation

 

    private Matrix _model;

    private Matrix _lastRotation;

 

    private double _rotation;

    private Vector3 _direction;

    private Vector3 _axis;

    private bool _justReset;

 

    private double _rotationInc;

    const double baseRotationInc = 0.05;

    const double maxRotationInc = 0.2;

 

    // Some structs for our vertex and sphere information

 

    struct BasicVertex

    {

      public Vector3 pos;  // position

      public Vector3 norm; // surface normal vector

    };

 

    struct SphereDefinition

    {

      public Vector3 instancePos;

      public Vector4 col;

      public float rad;

    }

 

    struct ConstantBuffer

    {

      public Matrix projToWorld;

      public Matrix model;

    }

 

    // Initializes a new instance of SphereRenderer

 

    public ApollonianRenderer(DeviceManager devices, int level)

    {

      Scale = 1.0f;

      Paused = true;

 

      _rotationInc = baseRotationInc;

      _rotation = 0;

      SetNewDirection(0, 1);

      _justReset = true;

 

      _level = level;

      _instCount = 0;

      _model = Matrix.Identity;

 

      // We need only create the geometry for a sphere once

 

      CreateSphereGeometry(out _vertices, out _indices);

      _vertCount = _vertices.Length;

      _idxCount = _indices.Length;

    }

 

    public float Scale { get; set; }

 

    public bool Paused { get; set; }

 

    // When the level is changed, we actually need to query

    // the web-service and process the returned data

 

    public int Level

    {

      get

      {

        return _level;

      }

      set

      {

        if (_level != value)

        {

          _level = value;

          CreateInstancesForLevel(_device, _level);

        }

      }

    }

 

    // Our event for the UI to be told when we're done loading

 

    public event RenderInitCompletedEventHandler OnInitCompleted;

 

    // Create the geometry representing a sphere

 

    static void CreateSphereGeometry(

      out BasicVertex[] vertices,

      out short[]indices

    )

    {

      // Determine the granularity of our polygons

 

      const int numSegs = 32;

      const int numSlices = numSegs / 2;

 

      // Collect the vertices for our triangles

 

      int numVerts = (numSlices + 1) * (numSegs + 1);

      vertices = new BasicVertex[numVerts];

 

      for (int slice=0; slice <= numSlices; slice++)

      {

        float v = (float)slice / (float)numSlices;

        float inclination = v * (float)Math.PI;

        float y = (float)Math.Cos(inclination);

        float r = (float)Math.Sin(inclination);

        for (int segment=0; segment <= numSegs; segment++)

        {

          float u = (float)segment / (float)numSegs;

          float azimuth = u * (float)Math.PI * 2.0f;

          int index = slice * (numSegs + 1) + segment;

          vertices[index].pos =

            new Vector3(

              r * (float)Math.Sin(azimuth),

              y,

              r * (float)Math.Cos(azimuth)

            );

          vertices[index].norm = vertices[index].pos;

        }

      }

 

      // Create the indices linking these vertices

 

      int numIndices = numSlices * (numSegs-2) * 6;

      indices = new short[numIndices];

 

      uint idx = 0;

      for (int slice=0;slice<numSlices;slice++)

      {

        ushort sliceBase0 = (ushort)((slice  )*(numSegs+1));

        ushort sliceBase1 = (ushort)((slice+1)*(numSegs+1));

        for (short segment=0;segment<numSegs;segment++)

        {

          if(slice>0)

          {

            indices[idx++] = (short)(sliceBase0 + segment);

            indices[idx++] = (short)(sliceBase0 + segment + 1);

            indices[idx++] = (short)(sliceBase1 + segment + 1);

          }

          if(slice<numSlices-1)

          {

            indices[idx++] = (short)(sliceBase0 + segment);

            indices[idx++] = (short)(sliceBase1 + segment + 1);

            indices[idx++] = (short)(sliceBase1 + segment);

          }

        }

      }

    }

 

    public virtual void Initialize(DeviceManager devices)

    {

      // Remove previous buffer

 

      SafeDispose(ref _constBuf);

 

      CreatePipeline(devices);

 

      CreateInstancesForLevel(

        devices.DeviceDirect3D, _level

      );

 

      _clock = new Stopwatch();

      _clock.Start();

    }

 

    // Create the Direct3D pipeline for our rendering

 

    private void CreatePipeline(DeviceManager devices)

    {

      // Setup local variables

 

      _device = devices.DeviceDirect3D;

 

      var path =

        Windows.ApplicationModel.Package.Current.

          InstalledLocation.Path;

 

      // Loads vertex shader bytecode

 

      var vertexShaderByteCode =

        NativeFile.ReadAllBytes(path + "\\SimpleSphere_VS.fxo");

      _vertShader =

        new VertexShader(_device, vertexShaderByteCode);

 

      // Loads pixel shader bytecode

 

      _pxlShader =

        new PixelShader(

          _device,

          NativeFile.ReadAllBytes(path + "\\SimpleSphere_PS.fxo")

        );

 

      // Layout from VertexShader input signature

 

      _layout =

        new InputLayout(

          _device,

          vertexShaderByteCode,

          new[]

          {

            // Per-vertex data

 

            new InputElement(

              "POSITION", 0, Format.R32G32B32_Float, 0, 0

            ),

            new InputElement(

              "NORMAL", 0, Format.R32G32B32_Float, 12, 0

            ),

 

            // Per-instance data

 

            // Instance position

 

            new InputElement(

              "TEXCOORD", 0, Format.R32G32B32_Float, 0, 1,

              InputClassification.PerInstanceData, 1

            ),

 

            // Instance colour

 

            new InputElement(

              "TEXCOORD", 1, Format.R32G32B32A32_Float, 12, 1,

              InputClassification.PerInstanceData, 1

            ),

 

            // Instance radius

 

            new InputElement(

              "TEXCOORD", 2, Format.R32_Float, 28, 1,

              InputClassification.PerInstanceData, 1

            )

          }

        );

    }

 

    // Access the Apollonian web-service and create the instance

    // information from the results

 

    private async void CreateInstancesForLevel(Device dev, int lev)

    {

      // Set up our various arrays and populate them

 

      SphereDefinition[] instances = null;

 

      try

      {

        instances = await SpheresForLevel(lev);

        _instCount = instances.Length;

 

        // Create our buffers

 

        _idxBuf =

          ToDispose(

            Buffer.Create(dev, BindFlags.IndexBuffer, _indices)

          );

 

        _vertBufs =

          new Buffer[]

          {

            ToDispose(

              Buffer.Create(dev, BindFlags.VertexBuffer, _vertices)

            ),

            ToDispose(

              Buffer.Create(dev, BindFlags.VertexBuffer, instances)

            )

          };

 

        _vertStrides =

          new int[]

          {

            Utilities.SizeOf<BasicVertex>(),

            Utilities.SizeOf<SphereDefinition>()

          };

 

        _vertOffsets = new int[] { 0, 0 };

 

        // Create Constant Buffer

 

        _constBuf =

          ToDispose(

            new Buffer(

              dev,

              Utilities.SizeOf<ConstantBuffer>(),

              ResourceUsage.Default,

              BindFlags.ConstantBuffer,

              CpuAccessFlags.None,

              ResourceOptionFlags.None,

              0

            )

          );

      }

      catch { }

 

      if (OnInitCompleted != null)

      {

        OnInitCompleted(this, new EventArgs());

      }

    }

 

    // Generate the sphere instance information for a

    // particular level

 

    private async Task<SphereDefinition[]> SpheresForLevel(

      int level

    )

    {

      string responseText = await GetJsonStream(level);

      return SpheresFromJson(responseText);

    }

 

    // Access our web-service asynchronously and return the

    // results

 

    private async Task<string> GetJsonStream(int level)

    {

      HttpClient client = new HttpClient();

      string url =

        "http://apollonian.cloudapp.net/api/spheres/1/" +

        level.ToString();

      client.MaxResponseContentBufferSize = 1500000;

      HttpResponseMessage response = await client.GetAsync(url);

      return await response.Content.ReadAsStringAsync();

    }

 

    // Extract the sphere definitions from the JSON data

 

    private static SphereDefinition[] SpheresFromJson(

      string responseText

    )

    {

      // Create our list to return and the list of colors

 

      var spheres = new List<SphereDefinition>();

      var colors =

        new Vector4[]

        {

          Colors.Black.ToVector4(),

          Colors.Red.ToVector4(),

          Colors.Yellow.ToVector4(),

          Colors.Green.ToVector4(),

          Colors.Cyan.ToVector4(),

          Colors.Blue.ToVector4(),

          Colors.Magenta.ToVector4(),

          Colors.DarkGray.ToVector4(),

          Colors.Gray.ToVector4(),

          Colors.LightGray.ToVector4(),

          Colors.White.ToVector4()

        };

 

      // Our data contains an array at its root

 

      JsonArray root = JsonArray.Parse(responseText);

      foreach (JsonValue val in root)

      {

        // Each value in the array is actually an object

 

        JsonObject obj = val.GetObject();

 

        // Extract the properties we need from each object

 

        SphereDefinition def;

        def.instancePos.X = (float)obj.GetNamedNumber("X");

        def.instancePos.Y = (float)obj.GetNamedNumber("Y");

        def.instancePos.Z = (float)obj.GetNamedNumber("Z");

        def.rad = (float)obj.GetNamedNumber("R");

        var level = (int)obj.GetNamedNumber("L");

        def.col = colors[level <= 10 ? level : 10];

 

        // Only add spheres near the edge of the outer one

 

        if (def.instancePos.Length() + def.rad > 0.99)

          spheres.Add(def);

      }

      return spheres.ToArray();

    }

 

    public void Swipe(float x, float y)

    {

      // Spins in a particular direction

 

      Paused = false;

 

      if (_rotation == 0.0)

      {

        // No existing animation

 

        SetNewDirection(x, y);

      }

      else

      {

        // Existing animation...

 

        if (SameDirection(x, y, _direction.X, _direction.Y))

        {

          // ... in the same direction as the swipe, so we

          // speed up the animation by halving the duration

 

          if ((_rotationInc * 2) <= maxRotationInc)

          {

            _rotationInc *= 2;

          }

        }

        else

        {

          // A new direction, reset the direction and increment

 

          SetNewDirection(x, y);

          _rotationInc = baseRotationInc;

          _rotation = 0;

 

          // Make sure we set the existing rotation as the model

 

          _model = _lastRotation *_model;

        }

      }

    }

 

    private void SetNewDirection(float x, float y)

    {

      _direction = new Vector3(x, y, 0f);

      _direction.Normalize();

      _axis =

        PerpendicularAxis(-_direction.X, _direction.Y);

      _axis.Normalize();

      _justReset = true;

    }

 

    // Touch-related helpers

 

    private bool SameSign(double a, double b)

    {

      return

        (

          (a > 0 && b > 0) ||

          (a < 0 && b < 0) ||

          (a == 0 && b == 0)

        );

    }

    private bool SameDirection(

      float x1, float y1, float x2, float y2

    )

    {

      if (!(SameSign(y1, y2) && SameSign(x1, x2)))

        return false;

      return

        Math.Abs(Math.Atan2(y1, x1) - Math.Atan2(y2, x2)) < 0.1;

    }

 

    private Vector3 PerpendicularAxis(float x, float y)

    {

      // Uses a fairly unsophisticated approach to generating

      // a perpendicular vector

 

      if (y == 0)

        return new Vector3(y, -x, 0);

      else

        return new Vector3(-y, x, 0);

    }

 

    // This is called in a loop

 

    public virtual void Render(TargetBase render)

    {

      if (_clock == null) return;

 

      var ctxt = render.DeviceManager.ContextDirect3D;

      var dev = render.DeviceManager.DeviceDirect3D;

 

      float width = (float)render.RenderTargetSize.Width;

      float height = (float)render.RenderTargetSize.Height;

 

      // Prepare matrices

 

      var view =

        Matrix.LookAtLH(

          new Vector3(0, 0, -5),

          new Vector3(0, 0, 0),

          Vector3.UnitY

        );

 

      var proj =

        Matrix.PerspectiveFovLH(

          (float)Math.PI / 4.0f,

          width / (float)height,

          0.1f,

          100.0f

        );

 

      var viewProj = Matrix.Multiply(view, proj);

 

      var time =

        (float)(_clock.ElapsedMilliseconds / 1000.0);

 

      // Set targets (this is mandatory in the loop)

 

      ctxt.OutputMerger.SetTargets(

        render.DepthStencilView,

        render.RenderTargetView

      );

 

      // Clear the views

 

      ctxt.ClearDepthStencilView(

        render.DepthStencilView,

        DepthStencilClearFlags.Depth,

        1.0f,

        0

      );

 

      ctxt.ClearRenderTargetView(

        render.RenderTargetView,

        Colors.Black

      );

 

      // If we have instances, let's display them

 

      if (_instCount > 0)

      {

        // Setup the pipeline

 

        ctxt.InputAssembler.InputLayout = _layout;

 

        ctxt.InputAssembler.PrimitiveTopology =

          PrimitiveTopology.TriangleList;

 

        ctxt.InputAssembler.SetVertexBuffers(

          0,

          _vertBufs,

          _vertStrides,

          _vertOffsets

        );

 

        ctxt.InputAssembler.SetIndexBuffer(

          _idxBuf,

          Format.R16_UInt,

          0

        );

 

        ctxt.VertexShader.SetConstantBuffer(

          0,

          _constBuf

        );

 

        ctxt.VertexShader.Set(_vertShader);

        ctxt.PixelShader.Set(_pxlShader);

 

        // Calculate worldViewProj

 

        if (!Paused && !_justReset)

          _rotation += _rotationInc;

        _justReset = false;

 

        _lastRotation =

          Matrix.RotationAxis(_axis, (float)_rotation);

 

        var worldViewProj =

          Matrix.Scaling(Scale) *

          _lastRotation *

          viewProj;

        worldViewProj.Transpose();

 

        // Create our constant buffer data to pass to the

        // vertex shader

 

        var cbuffer = new ConstantBuffer();

        cbuffer.projToWorld = worldViewProj;

        cbuffer.model = _model;

 

        // Update constant buffer

 

        ctxt.UpdateSubresource(ref cbuffer, _constBuf);

 

        // Draw the spheres

 

        ctxt.DrawIndexedInstanced(

          _idxCount,

          _instCount,

          0,

          0,

          0

        );

      }

    }

  }

}

There were some changes to other files, too, but the other most important one is App.Xaml.cs, as this hooks up the pointer input with our gesture recognizer in order to interpret gestures that get passed down to the renderer for execution:

using System;

using Windows.ApplicationModel;

using Windows.ApplicationModel.Activation;

using Windows.Graphics.Display;

using Windows.UI.Core;

using Windows.UI.Input;

using Windows.UI.Xaml;

using Windows.UI.Xaml.Media;

using CommonDX;

 

namespace ApollonianViewer

{

  // Provides application-specific behavior to supplement the

  // default Application class.

 

  sealed partial class App : Application

  {

    private DeviceManager _devices;

    private SwapChainBackgroundPanelTarget _target;

    private DirectXPanelXaml _swapchainPanel;

    private ApollonianRenderer _renderer;

    private GestureRecognizer _gr;

    private PointerPoint _slideStart;

    private bool _commandHappened;

    private double _prevScale;

 

    // Initializes the singleton application object.  This is

    // the first line of authored code executed, and as such is

    // the logical equivalent of main() or WinMain().

 

    public App()

    {

      this.InitializeComponent();

      this.Suspending += OnSuspending;

    }

 

    // Invoked when the application is launched normally by the

    // end user.  Other entry points will be used when the

    // application is launched to open a specific file, to display

    // search results, and so forth.

 

    protected override void OnLaunched(LaunchActivatedEventArgs args)

    {

      if (

        args.PreviousExecutionState ==

          ApplicationExecutionState.Terminated

      )

      {

      }

 

      // Create a new DeviceManager (Direct3D, Direct2D,

      // DirectWrite, WIC)

 

      _devices = new DeviceManager();

 

      // New SphereRenderer

 

      const int level = 5;

 

      _renderer = new ApollonianRenderer(_devices, level);

 

      _gr = new GestureRecognizer();

      _gr.GestureSettings =

        GestureSettings.Drag |

        GestureSettings.Tap |

        GestureSettings.ManipulationScale;

 

      _gr.ManipulationStarted += OnManipulationStarted;

      _gr.ManipulationUpdated += OnManipulationUpdated;

      _gr.ManipulationCompleted += OnManipulationCompleted;

      _gr.Dragging += OnDragging;

      _gr.Tapped += OnTapped;

 

      // In order for our GestureRecognizer to work

      var cw = Window.Current.CoreWindow;

      cw.PointerPressed += OnPointerPressed;

      cw.PointerMoved += OnPointerMoved;

      cw.PointerReleased += OnPointerReleased;

 

      // Place the frame in the current Window and ensure that it is

      // active

 

      _swapchainPanel = new DirectXPanelXaml(_renderer, _gr, level);

      Window.Current.Content = _swapchainPanel;

      Window.Current.Activate();

 

      // Use CoreWindowTarget as the rendering target (Initialize

      // SwapChain, RenderTargetView, DepthStencilView, BitmapTarget)

 

      _target =

        new SwapChainBackgroundPanelTarget(_swapchainPanel);

 

      // Add Initializer to device manager

 

      _devices.OnInitialize += _target.Initialize;

      _devices.OnInitialize += _renderer.Initialize;

 

      // Render the cube within the CoreWindow

 

      _target.OnRender += _renderer.Render;

 

      // Initialize the device manager and all registered

      // deviceManager.OnInitialize

 

      _devices.Initialize(DisplayProperties.LogicalDpi);

 

      // Setup rendering callback

 

      CompositionTarget.Rendering +=

        CompositionTarget_Rendering;

 

      // Callback on DpiChanged

 

      DisplayProperties.LogicalDpiChanged +=

        DisplayProperties_LogicalDpiChanged;

    }

 

    void OnPointerReleased(CoreWindow sender, PointerEventArgs args)

    {

      // Only process the swipe of another command (particularly

      // scale) has not already happened

 

      if (!_commandHappened && !_swapchainPanel.UIActive)

      {

        PointerPoint slideEnd = args.CurrentPoint;

        double xdiff = slideEnd.Position.X - _slideStart.Position.X;

        double ydiff = slideEnd.Position.Y - _slideStart.Position.Y;

 

        if (!(Math.Abs(xdiff) < 5 && Math.Abs(ydiff) < 5))

          _renderer.Swipe((float)xdiff, (float)ydiff);

      }

      _swapchainPanel.UIActive = false;

 

      _gr.ProcessUpEvent(args.CurrentPoint);

    }

 

    void OnPointerMoved(CoreWindow sender, PointerEventArgs args)

    {

      _gr.ProcessMoveEvents(args.GetIntermediatePoints());

    }

 

    void OnPointerPressed(CoreWindow sender, PointerEventArgs args)

    {

      // Record some data that may be used later in the

      // event sequence

 

      _slideStart = args.CurrentPoint;

      _prevScale = _renderer.Scale;

 

      // Reset the flag

 

      _commandHappened = false;

 

      // Pass through the data for our gestures to be recognised

 

      _gr.ProcessDownEvent(args.CurrentPoint);

    }

 

    void OnTapped(GestureRecognizer sender, TappedEventArgs args)

    {

      _renderer.Paused = !_renderer.Paused;

      _commandHappened = true;

    }

 

    void OnDragging(

      GestureRecognizer sender,

      DraggingEventArgs args

    )

    {

    }

 

    void OnManipulationStarted(

      GestureRecognizer sender,

      ManipulationStartedEventArgs args

    )

    {

    }

 

    void OnManipulationUpdated(

      GestureRecognizer sender,

      ManipulationUpdatedEventArgs args

    )

    {

      if (args.Cumulative.Scale != 1.0)

      {

        // Set the scale relative to the previous one and

        // update the slider in the UI

 

        _renderer.Scale =

          (float)(args.Cumulative.Scale * _prevScale);

        _swapchainPanel.SetSliderFromScale(_renderer.Scale);

        _commandHappened = true;

      }

    }

 

    void OnManipulationCompleted(

      GestureRecognizer sender,

      ManipulationCompletedEventArgs args

    )

    {

    }

 

    void DisplayProperties_LogicalDpiChanged(object sender)

    {

      _devices.Dpi = DisplayProperties.LogicalDpi;

    }

 

    void CompositionTarget_Rendering(object sender, object e)

    {

      _target.RenderAll();

      _target.Present();

    }

 

    void OnSuspending(object sender, SuspendingEventArgs e)

    {

    }

  }

}

And here’s the overall project, to see the various changes together. At this stage, I’d consider this version of the app basically complete, although there are a few minor quirks such as a slight jerkiness of the transition when changing the spin direction (this is certainly a coding issue on my part, rather than being the fault of WinRT/DirectX).

In the next post, we’ll revisit the sub-series on iOS before we wrap things up with a series summary.

Update:

The Windows 8 Release Preview resulted in some breaking API changes, causing the above project to no longer work. The main issue was with the internals of SharpDX – this required an update for it to work on this new OS version – and so updating the project to make use of SharpDX 2.2.0 was the main task. Beyond that, a small update to LayoutAwarePage.cs, – to remove a redundant event handler – and an update to StandardStyles.xaml (which I believe is a standard file created by the template for Metro-style apps) was needed. Other than that, it just worked. Here’s the updated project for those of you working with the Windows 8 Release Preview or beyond.

blog comments powered by Disqus

Feed/Share

10 Random Posts