A blog for developers programming with AutoCAD and other Autodesk platforms.

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;

usingBuffer = SharpDX.Direct3D11.Buffer;

usingDevice = SharpDX.Direct3D11.Device;

namespace ApollonianViewer

{

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

// once a level has been loaded completely

publicdelegatevoidRenderInitCompletedEventHandler(

object sender, EventArgs e

);

publicclassApollonianRenderer : Component

{

Device _device;

// Our core geometry data

BasicVertex[] _vertices;

short[] _indices;

privateint _vertCount;

privateint _idxCount;

// Our members to map this data for the GPU

privateInputLayout _layout;

privateBuffer[] _vertBufs;

privateint[] _vertStrides;

privateint[] _vertOffsets;

privateBuffer _idxBuf;

privateBuffer _constBuf;

privateint _instCount;

privateStopwatch _clock;

privateVertexShader _vertShader;

privatePixelShader _pxlShader;

// The current level we're rendering

privateint _level;

// Our model transformation

privateMatrix _model;

privateMatrix _lastRotation;

privatedouble _rotation;

privateVector3 _direction;

privateVector3 _axis;

privatebool _justReset;

privatedouble _rotationInc;

constdouble baseRotationInc = 0.05;

constdouble maxRotationInc = 0.2;

// Some structs for our vertex and sphere information

structBasicVertex

{

publicVector3 pos; // position

publicVector3 norm; // surface normal vector

};

structSphereDefinition

{

publicVector3 instancePos;

publicVector4 col;

publicfloat rad;

}

structConstantBuffer

{

publicMatrix projToWorld;

publicMatrix 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;

}

publicfloat Scale { get; set; }

publicbool Paused { get; set; }

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

// the web-service and process the returned data

publicint 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

publiceventRenderInitCompletedEventHandler OnInitCompleted;

// Create the geometry representing a sphere

staticvoid CreateSphereGeometry(

outBasicVertex[] vertices,

outshort[]indices

)

{

// Determine the granularity of our polygons

constint numSegs = 32;

constint numSlices = numSegs / 2;

// Collect the vertices for our triangles

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

vertices = newBasicVertex[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 =

newVector3(

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 = newshort[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);

}

}

}

}

publicvirtualvoid Initialize(DeviceManager devices)

{

// Remove previous buffer

SafeDispose(ref _constBuf);

CreatePipeline(devices);

CreateInstancesForLevel(

devices.DeviceDirect3D, _level

);

_clock = newStopwatch();

_clock.Start();

}

// Create the Direct3D pipeline for our rendering

privatevoid 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 =

newVertexShader(_device, vertexShaderByteCode);

// Loads pixel shader bytecode

_pxlShader =

newPixelShader(

_device,

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

);

// Layout from VertexShader input signature

_layout =

newInputLayout(

_device,

vertexShaderByteCode,

new[]

{

// Per-vertex data

newInputElement(

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

),

newInputElement(

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

),

// Per-instance data

// Instance position

newInputElement(

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

InputClassification.PerInstanceData, 1

),

// Instance colour

newInputElement(

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

InputClassification.PerInstanceData, 1

),

// Instance radius

newInputElement(

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

InputClassification.PerInstanceData, 1

)

}

);

}

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

// information from the results

privateasyncvoid 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 =

newBuffer[]

{

ToDispose(

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

),

ToDispose(

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

)

};

_vertStrides =

newint[]

{

Utilities.SizeOf<BasicVertex>(),

Utilities.SizeOf<SphereDefinition>()

};

_vertOffsets = newint[] { 0, 0 };

// Create Constant Buffer

_constBuf =

ToDispose(

newBuffer(

dev,

Utilities.SizeOf<ConstantBuffer>(),

ResourceUsage.Default,

BindFlags.ConstantBuffer,

CpuAccessFlags.None,

ResourceOptionFlags.None,

0

)

);

}

catch { }

if (OnInitCompleted != null)

{

OnInitCompleted(this, newEventArgs());

}

}

// Generate the sphere instance information for a

// particular level

privateasyncTask<SphereDefinition[]> SpheresForLevel(

int level

)

{

string responseText = await GetJsonStream(level);

return SpheresFromJson(responseText);

}

// Access our web-service asynchronously and return the

// results

privateasyncTask<string> GetJsonStream(int level)

{

HttpClient client = newHttpClient();

string url =

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

level.ToString();

client.MaxResponseContentBufferSize = 1500000;

HttpResponseMessage response = await client.GetAsync(url);

returnawait response.Content.ReadAsStringAsync();

}

// Extract the sphere definitions from the JSON data

privatestaticSphereDefinition[] SpheresFromJson(

string responseText

)

{

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

var spheres = newList<SphereDefinition>();

var colors =

newVector4[]

{

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();

}

publicvoid 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;

}

}

}

privatevoid SetNewDirection(float x, float y)

{

_direction = newVector3(x, y, 0f);

_direction.Normalize();

_axis =

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

_axis.Normalize();

_justReset = true;

}

// Touch-related helpers

privatebool SameSign(double a, double b)

{

return

(

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

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

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

);

}

privatebool SameDirection(

float x1, float y1, float x2, float y2

)

{

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

returnfalse;

return

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

}

privateVector3 PerpendicularAxis(float x, float y)

{

// Uses a fairly unsophisticated approach to generating

// a perpendicular vector

if (y == 0)

returnnewVector3(y, -x, 0);

else

returnnewVector3(-y, x, 0);

}

// This is called in a loop

publicvirtualvoid 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(

newVector3(0, 0, -5),

newVector3(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 = newConstantBuffer();

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.

sealedpartialclassApp : Application

{

privateDeviceManager _devices;

privateSwapChainBackgroundPanelTarget _target;

privateDirectXPanelXaml _swapchainPanel;

privateApollonianRenderer _renderer;

privateGestureRecognizer _gr;

privatePointerPoint _slideStart;

privatebool _commandHappened;

privatedouble _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.

protectedoverridevoid OnLaunched(LaunchActivatedEventArgs args)

{

if (

args.PreviousExecutionState ==

ApplicationExecutionState.Terminated

)

{

}

// Create a new DeviceManager (Direct3D, Direct2D,

// DirectWrite, WIC)

_devices = newDeviceManager();

// New SphereRenderer

constint level = 5;

_renderer = newApollonianRenderer(_devices, level);

_gr = newGestureRecognizer();

_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 = newDirectXPanelXaml(_renderer, _gr, level);

Window.Current.Content = _swapchainPanel;

Window.Current.Activate();

// Use CoreWindowTarget as the rendering target (Initialize

// SwapChain, RenderTargetView, DepthStencilView, BitmapTarget)

_target =

newSwapChainBackgroundPanelTarget(_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.