+1  A: 

My linear algebra is a little rusty be confident in helping, but this article looks like it could be a good starting place for you

BioBuckyBall
Thanks Lucas. A link out from the link you provided was a bit helpful, but I'm still overall at a loss here.
Otaku
+2  A: 

see this and this article. Search "perspective" in the page.

moldovanu
I had a good long look at both and unfortunately they don't help much. What I really need is a starter explanation against the samples I've provided above with the data provided above.
Otaku
+2  A: 

This seems to explain exactly your scenario

AlexEzh
First of all, that tarantula is scary. Secondly, I don't believe `<PlaneProjection/>` is the answer here (I could be wrong though) as according to Jaime Rodriguez on http://blogs.msdn.com/b/jaimer/archive/2009/06/03/silverlight3-planeprojection-primer.aspx, `<PlaneProjection/>` uses only an FOV of 57°. If you could demonstrate how it could be set to a different FOV with `<PlaneProjection/>` using the examples above (the X and FOV values), that would be great.
Otaku
+2  A: 

After lots of playing with this I actually concur with "Ladislav Mrnka"'s matrix answer as being the simplest solution and have up-voted their answer.

Just leaving the sample below to give you something to play with but you will need to update it via a Matrix3DProjection.

It looks like you are treating your source picture as having one of several possible fields of view, e.g. as if taken with a wide-angle lens for the 120° or a zoom lens for the 30°. You are then trying to reproduce the aspect ratio of the original scene when displayed. Is this correct?

If so the procedure you actually want to stretch the picture horizontally to restore the implicit width, before rotating it with the perspective transform. That would mean you are actually trying to solve 2 separate (simpler) maths problems here e.g.:

  • Calculate the display width of the image based on FOV, aspect ratio & width (using X-Scaling).
  • Calculate the rotation desired to fit a perspective transform of a given width within a desired display width (projection rotation about Y axis).

The difficulty I have is that the example photos do not indicate any specific rules about the display. The display widths all vary so I cannot work out what your end result is intended to be. If you can provide more information I should be able to provide specific calculations.

Ok, based on your use of the Perspective settings in PowerPoint, the 2 required steps are indeed:

  • Scale the horizontal size (according to your "X"-angle)
  • Apply a projection transform to emulate the perspective angle in PowerPoint

The first calculation is very simple. You need to set the scale to Cosine(X-angle). The second is an estimate as the Powerpoint perspective angle does not seem to relate to rotation.

I have provided a full sample XAML and code-behind below to generate the app shown*.

alt text

**Note: there is a serious flaw in that Projection Transforms are not able to distort the image to the degree you require. I am trying Matrix3DProjection instead, solution will follow *

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Class="PerspectivePhotosTest.PerspectivePhotos"
    d:DesignWidth="640" d:DesignHeight="480">

    <Grid x:Name="LayoutRoot">
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <Image x:Name="SampleImage" Height="101" Source="Image1.png" Stretch="Fill" VerticalAlignment="Center" HorizontalAlignment="Center" Width="128" RenderTransformOrigin="0.5,0.5">
                <Image.Projection>
                    <PlaneProjection x:Name="Rotation" RotationY="0"/>
                </Image.Projection>
                <Image.RenderTransform>
                    <CompositeTransform x:Name="Scale" ScaleX="1"/>
                </Image.RenderTransform>
            </Image>
            <Grid HorizontalAlignment="Left" VerticalAlignment="Top">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBlock Text="&quot;X-Angle&quot;" VerticalAlignment="Top" HorizontalAlignment="Right"/>
                <TextBox x:Name="XAngleTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=XValueSlider}" TextChanged="XAngleTextBox_TextChanged"/>
                <Slider x:Name="XValueSlider" Grid.Row="1" Grid.ColumnSpan="2" LargeChange="10" Maximum="80" SmallChange="1"/>
                <TextBlock Text="Perspective Angle" VerticalAlignment="Top" Grid.Row="2" HorizontalAlignment="Right"/>
                <TextBox x:Name="PerspectiveAngleTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=PerspectiveSlider}" TextChanged="PerspectiveAngleTextBox_TextChanged"/>
                <Slider x:Name="PerspectiveSlider" Grid.Row="3" Grid.ColumnSpan="2" Maximum="120" SmallChange="1" LargeChange="10"/>
            </Grid>
        </StackPanel>
    </Grid>
</UserControl>

Code behind:

using System;
using System.Windows.Controls;

namespace PerspectivePhotosTest
{
    public partial class PerspectivePhotos : UserControl
    {
        public PerspectivePhotos()
        {
            InitializeComponent();
        }

        private void XAngleTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {
            Scale.ScaleX = CalcScale(DegreeToRadian(double.Parse(XAngleTextBox.Text)));
        }

        private void PerspectiveAngleTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {
            Rotation.RotationY = CalcTransform(double.Parse(PerspectiveAngleTextBox.Text));
        }

        private double CalcScale(double angleInRadians)
        {
            return Math.Cos(angleInRadians) * 2 + 0.3;
        }

        private double CalcTransform(double angleInDegrees)
        {
            return angleInDegrees / 2;
        }

        private double DegreeToRadian(double angle)
        {
            return Math.PI * angle / 180.0;
        }
    }
}

This should give you a handle test framework to try out variations. I re-factored the calculation steps to make it more obvious.

Enough already
Thanks HiTech! What other information can I provide? The above transforms are easily reproduced in PowerPoint 2007 (3D Rotation > Perspective Preset > X=value, Perspective=FOV.
Otaku
I have PowerPoint 2007 so that was very useful and I am investigating their way of doing it now. Back to first principles though, for your needs do you require an exact copy of the PowerPoint algorithm for some reason, or just want something that looks "right"?
Enough already
In the meantime, I have studied the Powerpoint perspective transform and they basically are combining a linear rotation (only scales the image) with a "perspective" value to create a stretched projection rotation. The same effect can be created using my aforementioned scale then projection rotation suggestion. I will try work out the general maths example for you and provide tweak values so it will look similar to the PowerPoint one. As you only seem to require the single dimensional movement using matrix maths would be overkill (and harder to explain).
Enough already
I have updated the answer to give you a sample app to play with the settings. As I mentioned I cannot get the projection transform in Silverlight to add sufficient perspective compared to the PowerPoint version. I am reworking it to use Matrix3DProjection.
Enough already
@HiTech Magic: I really appreciate the effort you put into this.
Otaku
+2  A: 

Use Matrix3DProjection and set transformation based on matrix provided at the end of this function from Direct3D. You need your FOV in radians, Aspect ratio for screen and two distances for clipping (you are defining finite frustrum). If are you looking for futher explanation why it is set this way you should get some book about computer graphics. Also it is usual that matrix for projection transformation sets only view frustrum. Rotatin objects around X axis is performed by separate transformation but it is general practice from computer graphics and I'm not sure if it works same in Silverlight.

Edit:

If you need to use both rotation and projection in single matrix try to use this one:

xScale  0              0                     0
0       cos(X)*yScale  -sin(X)*z*zf/(zf-zn)  -sin(X)
0       sin(X)*yScale  cox(X)*z*zf/(zf-zn)   cos(X)
0       0              (-zn*zf)/(zf-zn)      0

Where X in cos(X) and sin(X) is your rotation around X asis in radians

z is translation in Z direction because you will have to move with your image to see it whole.

yScale = cot(FOV/2) FOV is in radians

xScale = yScale/aspectRatio Aspect ratio is defined by height and width of panel used for image rendering

zn = Z near - everything before this is clipped. zf = Z far - evrything after this is clipped. Be aware that z coordinate of image has to be between those two.

It is long time since I did this last time so I hope I have computed transformation correctly. Matrix multiplication should be correct but there is a chance that I multiplyed it in wrong order.

Edit2:

My previous suggestion doesn't work. First matrices used for computation are incorrect because Silverlight uses transposed versions. Second translation image to center and viewport transformation is not used. I have combined Alison's proposed code (also can be found here) with modification to have FovX and HiTech Magic's Silverlight application. I have never written Silverlight app before ... Here is working sample:

<Grid x:Name="LayoutRoot" Background="White" Loaded="LayoutRoot_Loaded">
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid x:Name="Canvas" Background="White" Height="150" Width="200">
            <Image x:Name="SampleImage" Source="Penguins.jpg" Stretch="Fill" VerticalAlignment="Center" HorizontalAlignment="Center" Height="120" Width="160" RenderTransformOrigin="0.5,0.5" >
                <Image.Projection>
                    <Matrix3DProjection x:Name="Matrix" ProjectionMatrix="1, 0, 0, 0, 
                                                                      0, 1, 0, 0, 
                                                                      0, 0, 1, 0, 
                                                                      0, 0, 0, 1"/>
                </Image.Projection>
            </Image>
        </Grid>
        <Grid HorizontalAlignment="Center" VerticalAlignment="Top">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="&quot;X-Angle&quot;" VerticalAlignment="Top" HorizontalAlignment="Right"/>
            <TextBox x:Name="XAngleTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=XValueSlider}" TextChanged="XAngleTextBox_TextChanged"/>
            <Slider x:Name="XValueSlider" Grid.Row="1" Grid.ColumnSpan="2" LargeChange="10" Maximum="80" SmallChange="1"/>
            <TextBlock Text="Perspective Angle" VerticalAlignment="Top" Grid.Row="2" HorizontalAlignment="Right"/>
            <TextBox x:Name="PerspectiveAngleTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=PerspectiveSlider}" TextChanged="PerspectiveAngleTextBox_TextChanged" />
            <Slider x:Name="PerspectiveSlider" Grid.Row="3" Grid.ColumnSpan="2" Maximum="120" SmallChange="1" LargeChange="10" Value="60"/>
        </Grid>
    </StackPanel>
</Grid>



public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void UpdateMatrix()
    {
        double val;


        double X = double.TryParse(XAngleTextBox.Text, NumberStyles.Any, 
            CultureInfo.InvariantCulture, out val) ? val : 0.0;
        double FOV = double.TryParse(PerspectiveAngleTextBox.Text, NumberStyles.Any, 
            CultureInfo.InvariantCulture, out val) ? val : 0.0;

        ApplyProjection(FOV, X);
    }

    private double DegreeToRadian(double angle)
    {
        return Math.PI * angle / 180.0;
    }

    private void XAngleTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
    {
        UpdateMatrix();
    }

    private void PerspectiveAngleTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
    {
        UpdateMatrix();
    }

    private void ApplyProjection(double FOV, double X)
    {
        // Translate the image along the negative Z-axis such that it occupies 50% of the
        // vertical field of view.
        double fov = DegreeToRadian(FOV);
        double translationZ = -SampleImage.Width / Math.Tan(fov / 2.0); 
        double theta = DegreeToRadian(X);

        // You can create a 3D effect by creating a number of simple 
        // tranformation Matrix3D matrixes and then multiply them together.
        Matrix3D centerImageAtOrigin = TranslationTransform(
                 -SampleImage.Width / 2.0,
                 -SampleImage.Height / 2.0, 0);
        Matrix3D invertYAxis = CreateScaleTransform(1.0, -1.0, 1.0);
        Matrix3D rotateAboutY = RotateYTransform(theta);
        Matrix3D translateAwayFromCamera = TranslationTransform(0, 0, translationZ);
        Matrix3D perspective = PerspectiveTransformFovRH(fov,
                Canvas.ActualWidth / Canvas.ActualHeight,   
                1.0,                                                // near plane
                1000.0);                                            // far plane
        Matrix3D viewport = ViewportTransform(Canvas.ActualWidth, Canvas.ActualHeight);

        Matrix3D m = centerImageAtOrigin * invertYAxis;
        m = m * rotateAboutY;
        m = m * translateAwayFromCamera;
        m = m * perspective;
        m = m * viewport;

        Matrix3DProjection m3dProjection = new Matrix3DProjection();
        m3dProjection.ProjectionMatrix = m;

        SampleImage.Projection = m3dProjection;
    }

    private Matrix3D TranslationTransform(double tx, double ty, double tz)
    {
        Matrix3D m = new Matrix3D();

        m.M11 = 1.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
        m.M21 = 0.0; m.M22 = 1.0; m.M23 = 0.0; m.M24 = 0.0;
        m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
        m.OffsetX = tx; m.OffsetY = ty; m.OffsetZ = tz; m.M44 = 1.0;

        return m;
    }

    private Matrix3D CreateScaleTransform(double sx, double sy, double sz)
    {
        Matrix3D m = new Matrix3D();

        m.M11 = sx; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
        m.M21 = 0.0; m.M22 = sy; m.M23 = 0.0; m.M24 = 0.0;
        m.M31 = 0.0; m.M32 = 0.0; m.M33 = sz; m.M34 = 0.0;
        m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;

        return m;
    }

    private Matrix3D RotateYTransform(double theta)
    {
        double sin = Math.Sin(theta);
        double cos = Math.Cos(theta);

        Matrix3D m = new Matrix3D();

        m.M11 = cos; m.M12 = 0.0; m.M13 = -sin; m.M14 = 0.0;
        m.M21 = 0.0; m.M22 = 1.0; m.M23 = 0.0; m.M24 = 0.0;
        m.M31 = sin; m.M32 = 0.0; m.M33 = cos; m.M34 = 0.0;
        m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;

        return m;
    }

    private Matrix3D RotateZTransform(double theta)
    {
        double cos = Math.Cos(theta);
        double sin = Math.Sin(theta);

        Matrix3D m = new Matrix3D();
        m.M11 = cos; m.M12 = sin; m.M13 = 0.0; m.M14 = 0.0;
        m.M21 = -sin; m.M22 = cos; m.M23 = 0.0; m.M24 = 0.0;
        m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
        m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
        return m;
    }

    private Matrix3D PerspectiveTransformFovRH(double fieldOfView, double aspectRatio, double zNearPlane, double zFarPlane)
    {
        double width = 1.0 / Math.Tan(fieldOfView / 2.0);
        double height = width * aspectRatio;
        double d = zNearPlane - zFarPlane;

        Matrix3D m = new Matrix3D();
        m.M11 = width; m.M12 = 0; m.M13 = 0; m.M14 = 0;
        m.M21 = 0; m.M22 = height; m.M23 = 0; m.M24 = 0;
        m.M31 = 0; m.M32 = 0; m.M33 = zFarPlane / d; m.M34 = -1;
        m.OffsetX = 0; m.OffsetY = 0; m.OffsetZ = zNearPlane * zFarPlane / d; m.M44 = 0;

        return m;
    }

    private Matrix3D ViewportTransform(double width, double height)
    {
        Matrix3D m = new Matrix3D();

        m.M11 = width / 2.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
        m.M21 = 0.0; m.M22 = -height / 2.0; m.M23 = 0.0; m.M24 = 0.0;
        m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
        m.OffsetX = width / 2.0; m.OffsetY = height / 2.0; m.OffsetZ = 0.0; m.M44 = 1.0;

        return m;
    }

    private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
    {
        UpdateMatrix();
    }

}
Ladislav Mrnka
+1  A: 

Okay. I have combined Ladislav Mrnka's Matrix transform answer with my previous sample app, but there appear to have been some typos in their 3D Matrix example and I am not strong enough with 3D Matrix math to correct it. The end result is a blank display where the image should be :(

Instead to get this party started I have provide a full interactive test app (below) with Xaml so that Ladislav Mrnka (or someone else with better 3D maths) can correct the problem.

<Grid x:Name="LayoutRoot" Background="White">
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <Image x:Name="SampleImage" Height="101" Source="Image1.png" Stretch="Fill" VerticalAlignment="Center" HorizontalAlignment="Center" Width="128" RenderTransformOrigin="0.5,0.5">
            <Image.Projection>
                <Matrix3DProjection x:Name="Matrix" ProjectionMatrix="1, 0, 0, 0,
                                                                      0, 1, 0, 0,
                                                                      0, 0, 1, 0,
                                                                      0, 0, 0, 1"/>
            </Image.Projection>
        </Image>
        <Grid HorizontalAlignment="Left" VerticalAlignment="Top">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="&quot;X-Angle&quot;" VerticalAlignment="Top" HorizontalAlignment="Right"/>
            <TextBox x:Name="XAngleTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=XValueSlider}" TextChanged="XAngleTextBox_TextChanged"/>
            <Slider x:Name="XValueSlider" Grid.Row="1" Grid.ColumnSpan="2" LargeChange="10" Maximum="80" SmallChange="1"/>
            <TextBlock Text="Perspective Angle" VerticalAlignment="Top" Grid.Row="2" HorizontalAlignment="Right"/>
            <TextBox x:Name="PerspectiveAngleTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=PerspectiveSlider}" TextChanged="PerspectiveAngleTextBox_TextChanged"/>
            <Slider x:Name="PerspectiveSlider" Grid.Row="3" Grid.ColumnSpan="2" Maximum="120" SmallChange="1" LargeChange="10"/>
            <TextBlock Text="Z-Near" VerticalAlignment="Top" Grid.Row="4" HorizontalAlignment="Right"/>
            <TextBox x:Name="ZNearTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=ZNearSlider}" TextChanged="ZNearTextBox_TextChanged"/>
            <Slider x:Name="ZNearSlider" Grid.Row="5" Grid.ColumnSpan="2" Minimum="-1000" Maximum="1000" SmallChange="1" LargeChange="10"/>
            <TextBlock Text="Z-Far" VerticalAlignment="Top" Grid.Row="6" HorizontalAlignment="Right"/>
            <TextBox x:Name="ZFarTextBox" d:LayoutOverrides="Height" Grid.ColumnSpan="2" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="40" Text="{Binding Value, ElementName=ZFarSlider}" TextChanged="ZFarTextBox_TextChanged"/>
            <Slider x:Name="ZFarSlider" Grid.Row="7" Grid.ColumnSpan="2" Minimum="-1000" Maximum="1000" SmallChange="1" LargeChange="10"/>
        </Grid>
    </StackPanel>
</Grid>

Code-behind:

using System;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace PerspectivePhotoTest
{
    public partial class PerspectivePhotosMatrix : UserControl
    {
        public PerspectivePhotosMatrix()
        {
            InitializeComponent();
        }

        private void UpdateMatrix()
        {
            double X = DegreeToRadian(double.Parse(XAngleTextBox.Text));
            double xScale = X;
            double FOV = DegreeToRadian(double.Parse(PerspectiveAngleTextBox.Text));
            double yScale = Math.Cos(FOV / 2) / Math.Sin(FOV / 2);
            double zn = double.Parse(ZNearTextBox.Text);
            double zf = double.Parse(ZFarTextBox.Text);
            double z = 0;

            Matrix.ProjectionMatrix = new Matrix3D(xScale, 0, 0, 0,
                0, Math.Cos(X) * yScale, -Math.Sin(X) * z * zf / (zf - zn), -Math.Sin(X),
                0, Math.Sin(X) * yScale, Math.Cos(X) * z * zf / (zf - zn), Math.Cos(X),
                0,0,(-zn*zf)/(zf-zn), 0);
        }

        private double DegreeToRadian(double angle)
        {
            return Math.PI * angle / 180.0;
        }

        private void XAngleTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {
            UpdateMatrix();
        }

        private void PerspectiveAngleTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {
            UpdateMatrix();
        }

        private void ZNearTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            UpdateMatrix();
        }

        private void ZFarTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            UpdateMatrix();
        }
    }
}
Enough already
+4  A: 

I think the problem everyone is encountering is that there needs to be a viewport shift along with the perspective transformation.

Try this out:

private void ApplyProjection()
{
    double fovY = FOV * Math.PI / 180 / 2.0;
    double translationZ = -image1.ActualHeight / Math.Tan(fovY / 2.0);
    double theta = YourAngleX * Math.PI / 180.0;

    Matrix3D centerImageAtOrigin = TranslationTransform(
             -image1.ActualWidth / 2.0,
             -image1.ActualHeight / 2.0, 0);
    Matrix3D invertYAxis = CreateScaleTransform(1.0, -1.0, 1.0);
    Matrix3D rotateAboutY = RotateYTransform(theta);
    Matrix3D translateAwayFromCamera = TranslationTransform(0, 0, translationZ);
    Matrix3D perspective = PerspectiveTransformFovRH(fovY,
            image1.ActualWidth / image1.ActualHeight,   // aspect ratio
            1.0,                                                // near plane
            1000.0);                                            // far plane
    Matrix3D viewport = ViewportTransform(image1.ActualWidth, image1.ActualHeight);

    Matrix3D m = centerImageAtOrigin * invertYAxis;
    m = m * rotateAboutY;
    m = m * translateAwayFromCamera;
    m = m * perspective;
    m = m * viewport;

    Matrix3DProjection m3dProjection = new Matrix3DProjection();
    m3dProjection.ProjectionMatrix = m;

    image1.Projection = m3dProjection;
}

private Matrix3D TranslationTransform(double tx, double ty, double tz)
{
    Matrix3D m = new Matrix3D();

    m.M11 = 1.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
    m.M21 = 0.0; m.M22 = 1.0; m.M23 = 0.0; m.M24 = 0.0;
    m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
    m.OffsetX = tx; m.OffsetY = ty; m.OffsetZ = tz; m.M44 = 1.0;

    return m;
}

private Matrix3D CreateScaleTransform(double sx, double sy, double sz)
{
    Matrix3D m = new Matrix3D();

    m.M11 = sx; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
    m.M21 = 0.0; m.M22 = sy; m.M23 = 0.0; m.M24 = 0.0;
    m.M31 = 0.0; m.M32 = 0.0; m.M33 = sz; m.M34 = 0.0;
    m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;

    return m;
}

private Matrix3D RotateYTransform(double theta)
{
    double sin = Math.Sin(theta);
    double cos = Math.Cos(theta);

    Matrix3D m = new Matrix3D();

    m.M11 = cos; m.M12 = 0.0; m.M13 = -sin; m.M14 = 0.0;
    m.M21 = 0.0; m.M22 = 1.0; m.M23 = 0.0; m.M24 = 0.0;
    m.M31 = sin; m.M32 = 0.0; m.M33 = cos; m.M34 = 0.0;
    m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;

    return m;
}

private Matrix3D RotateZTransform(double theta)
{
    double cos = Math.Cos(theta);
    double sin = Math.Sin(theta);

    Matrix3D m = new Matrix3D();
    m.M11 = cos; m.M12 = sin; m.M13 = 0.0; m.M14 = 0.0;
    m.M21 = -sin; m.M22 = cos; m.M23 = 0.0; m.M24 = 0.0;
    m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
    m.OffsetX = 0.0; m.OffsetY = 0.0; m.OffsetZ = 0.0; m.M44 = 1.0;
    return m;
}

private Matrix3D PerspectiveTransformFovRH(double fieldOfViewY, double aspectRatio, double zNearPlane, double zFarPlane)
{
    double height = 1.0 / Math.Tan(fieldOfViewY / 2.0);
    double width = height / aspectRatio;
    double d = zNearPlane - zFarPlane;

    Matrix3D m = new Matrix3D();
    m.M11 = width; m.M12 = 0; m.M13 = 0; m.M14 = 0;
    m.M21 = 0; m.M22 = height; m.M23 = 0; m.M24 = 0;
    m.M31 = 0; m.M32 = 0; m.M33 = zFarPlane / d; m.M34 = -1;
    m.OffsetX = 0; m.OffsetY = 0; m.OffsetZ = zNearPlane * zFarPlane / d; m.M44 = 0;

    return m;
}

private Matrix3D ViewportTransform(double width, double height)
{
    Matrix3D m = new Matrix3D();

    m.M11 = width / 2.0; m.M12 = 0.0; m.M13 = 0.0; m.M14 = 0.0;
    m.M21 = 0.0; m.M22 = -height / 2.0; m.M23 = 0.0; m.M24 = 0.0;
    m.M31 = 0.0; m.M32 = 0.0; m.M33 = 1.0; m.M34 = 0.0;
    m.OffsetX = width / 2.0; m.OffsetY = height / 2.0; m.OffsetZ = 0.0; m.M44 = 1.0;

    return m;
}

This will create the appropriate perspective shift and match what PowerPoint is producing.

This code was adapted from MSDN.

Alison
What is the `fovY` value and `theta`? Do those relate to my examples above?
Otaku
fovY refers to your field of view. theta refers to your AngleX. I've updated both to be a bit more clear. Let me know if you have problems running the sample.
Alison
Yes good point with viewport. It almost works but it still needs some update. I'm surprised that matrices are transposed. That is the reason why my matrix is completly wrong.
Ladislav Mrnka
It is surprising that this question has been so difficult to answer given how simple it appears to be on the surface. In WPF, this would be no problem but in Silverlight it is a real challenge.
Alison
+1, this is working out pretty well, thank you! The picture sort of jumps to a different location though (the middle of the screen). Is there a way to ensure the original CenterX and CenterY stay the same?
Otaku
Check code in my answer. I have solved the problem with jumping image. The trick is to place image to separate grid and use that grid for cumputation instead of LayoutRoot.
Ladislav Mrnka
It sounds like a bad idea to wrap the image in a grid. You're adding additional XAML to the page. What if you don't know at compile-time which image will be transformed? You can't place a grid around every image. You can achieve the same effect by replacing "LayoutRoot" with "image1". In this way, the image will stay in place.
Alison
Perfect, this is working great now!
Otaku