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:
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.