Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Set this Kind of Perspective Transform in Matrix3D?

I have an image with and have a few values to make it a perspective in Silverlight, but can't quite figure out what I need to do mathmatically to make it happen. The most important thing is I have an angle called a "Field of View" (FOV).

This is the normal picture: normal

For example:

X =   30°             X =   30°             X   =  30°
FOV = 30°             FOV = 60°             FOV = 120°


X =   60°             X =   60°               X =  60°
FOV = 30°             FOV = 60°             FOV = 120°

Any help would be appreciated to walk me through the math to reproduce these in Silverlight.

like image 681
Todd Main Avatar asked Jul 14 '10 21:07

Todd Main


2 Answers

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.

like image 133
NakedBrunch Avatar answered Nov 15 '22 16:11

NakedBrunch


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.

like image 25
Gone Coding Avatar answered Nov 15 '22 18:11

Gone Coding