360 파노라마 뷰어




목차


좀 철지난 주제이긴 합니다만, 포스팅을 해 보고자 합니다. ^-^;

요즘은 구글 스트릿 뷰, 다음 로드뷰, 네이버 등등 360도 파노라마 서비스를 하는 곳이 많이 있습니다. 하지만 제가 대학 졸업작품을 만들 당시만 하더라도, 구글 스트릿 뷰 이외에는 찾아보기 힘들었지요.

아시다시피 이런 로드뷰는 360도 파노라마 사진을 찍는데서부터 시작합니다. 전용 카메라로 촬영을 하지요.

이렇게 촬영된 사진은 아래처럼 휘어져(외곡되어) 있습니다.




요즘은 워낙 일반화되어서, 360 파노라마 사진을 구하기가 쉽네요. 예전엔 이런 이미지조차 구할수가 없어서, 구글 스트릿뷰에서 낱장 이미지를 일일이 뜯어낸 뒤, 합쳐서 만들었는데;;; 


모쪼록 이렇게 외곡된 이미지를 가지고 어떻게 360 파노라마 뷰를 만드는지 알아봅시다.




개요


파노라마 사진을 찍는 장비를 보셔서 아시겠지만, 말 그대로 중심(카메라)을 바탕으로 360도 화면 전체를 촬영해서 합쳐 만든 것입니다. 그렇다면, 이것을 다시 보려면 어떻게 하면 될까요? 원리는 간단합니다.


3D 공간을 만들고, 이곳에 텍스쳐로 이미지를 붙이면 됩니다. 그렇다면 3D 공간은 어떤 모양(shape)이어야 할까요? 바로 '구' 입니다. '구'를 만들고 그 중심에 '카메라' 를 넣고, 빛을 비추어 주면 촬영당시 공간에 있는것처럼 재현할수 있습니다. '구(sphere)', '카메라(camera)', '빛(light)' 는 3D 에서 말하는 요소들입니다. :)


WPF 로 구현해 보도록 하겠습니다.

코드는 보시면 아시겠지만, 어려울게 별로 없습니다. 대부분 xaml 로 작성하였고, cs 파일로 작성한 부분은 마우스로 카메라를 회전하는 코드 뿐입니다.

사실 이 부분도 약간의 트릭이 있습니다.

본래 사람이 움직인다고 생각해 보면,,,


'구' 모양의 방(공간)에 사람이 들어갑니다. 그리고 그 방의 벽에 그림(파노라마 이미지)이 그려져 있습니다. 이 사람을 정면을 바라보고 있습니다. 오른쪽을 바라보려면 어떻게 해야 할까요?

사람(카메라)이 오른쪽으로 돌아야 합니다. 그게 끝일까요? 아닙니다. 빛도 함께 회전해 주어야 합니다. 사람이 바라보는 방향으로 빛도 함께 이동되어야 하지요. 사람, 빛 두가지가 동시에 회전되어야 합니다.


다른 시각으로 한번 생각해보죠.

사람과 빛은 항상 정면을 바라봅니다. 그런데 방(공간)이 회전합니다. 그러면 어떻게 될까요? 맞습니다. 위 방식과 동일한 효과를 볼 수 있습니다. 방 하나만 회전하면 말이죠. 상대적인 관점에서 봤을때, 똑같다는 이야깁니다 :) 코드 구현하기도 이게 더 쉽겠지요. 공간 하나만 회전하면 되니까요.


다음 코드도 사람과 빛이 아니라, 공간을 회전시키므로써, 이 효과를 내고 있습니다. :)



참, cs 파일에 회전을 위한 마우스 드래그 코드 이외에, wpf 의 구 모양의 3d geometry 를 생성하는 코드도 포함되어 있습니다. 







    [MainWindow.xaml]

<Window x:Class="Study.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:study="clr-namespace:Study"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <study:PanoramaView PanoramaImage="Panorama1.jpg"></study:PanoramaView>
    </Grid>
</Window>


[PanoramaView.xaml]


<UserControl x:Class="Study.PanoramaView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             xmlns:local="clr-namespace:Study"
             d:DesignHeight="300" d:DesignWidth="300">
    <Viewport3D>
        <Viewport3D.Camera>
            <PerspectiveCamera Position="0,0,0" UpDirection="0,1,0" LookDirection="0,0,1" FieldOfView="95"></PerspectiveCamera>
        </Viewport3D.Camera>

        <ModelVisual3D>
            <ModelVisual3D.Content>
                <Model3DGroup>
                    <DirectionalLight Color="White" Direction="0,0,1"/>
                </Model3DGroup>
            </ModelVisual3D.Content>
        </ModelVisual3D>
        
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <GeometryModel3D Geometry="{x:Static local:PanoramaView.SphereModel}">
                    <GeometryModel3D.BackMaterial>
                        <DiffuseMaterial>
                            <DiffuseMaterial.Brush>
                                <ImageBrush ImageSource="{Binding Path=PanoramaImage, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
                            </DiffuseMaterial.Brush>
                        </DiffuseMaterial>
                    </GeometryModel3D.BackMaterial>
                </GeometryModel3D>
            </ModelVisual3D.Content>
            <ModelVisual3D.Transform>
                <Transform3DGroup>
                    <ScaleTransform3D ScaleX="1" ScaleY="1.5" ScaleZ="1"/>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D Angle="{Binding Path=RotateX, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Axis="0,1,0"/>
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D Angle="{Binding Path=RotateY, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Axis="1,0,0"/>
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                </Transform3DGroup>
            </ModelVisual3D.Transform>
        </ModelVisual3D>
    </Viewport3D>
</UserControl>




    [PanoramaView.cs]


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Study
{
    /// <summary>
    /// Interaction logic for PanoramaView.xaml
    /// </summary>
    public partial class PanoramaView : UserControl
    {
        public static Geometry3D SphereModel = CreateGeometry();



        public ImageSource PanoramaImage
        {
            get { return (ImageSource)GetValue(PanoramaImageProperty); }
            set { SetValue(PanoramaImageProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PanoramaImage.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PanoramaImageProperty =
            DependencyProperty.Register("PanoramaImage", typeof(ImageSource), typeof(PanoramaView), new PropertyMetadata(null));




        public PanoramaView()
        {
            InitializeComponent();
        }


        private static Geometry3D CreateGeometry()
        {
            int tDiv = 64;
            int yDiv = 64;
            double maxTheta = (360.0 / 180.0) * Math.PI;
            double minY = -1.0;
            double maxY = 1.0;

            double dt = maxTheta / tDiv;
            double dy = (maxY - minY) / yDiv;

            MeshGeometry3D mesh = new MeshGeometry3D();

            for (int yi = 0; yi <= yDiv; yi++)
            {
                double y = minY + yi * dy;

                for (int ti = 0; ti <= tDiv; ti++)
                {
                    double t = ti * dt;

                    mesh.Positions.Add(GetPosition(t, y));
                    mesh.Normals.Add(GetNormal(t, y));
                    mesh.TextureCoordinates.Add(GetTextureCoordinate(t, y));
                }
            }

            for (int yi = 0; yi < yDiv; yi++)
            {
                for (int ti = 0; ti < tDiv; ti++)
                {
                    int x0 = ti;
                    int x1 = (ti + 1);
                    int y0 = yi * (tDiv + 1);
                    int y1 = (yi + 1) * (tDiv + 1);

                    mesh.TriangleIndices.Add(x0 + y0);
                    mesh.TriangleIndices.Add(x0 + y1);
                    mesh.TriangleIndices.Add(x1 + y0);

                    mesh.TriangleIndices.Add(x1 + y0);
                    mesh.TriangleIndices.Add(x0 + y1);
                    mesh.TriangleIndices.Add(x1 + y1);
                }
            }

            mesh.Freeze();
            return mesh;
        }


        internal static Point3D GetPosition(double t, double y)
        {
            double r = Math.Sqrt(1 - y * y);
            double x = r * Math.Cos(t);
            double z = r * Math.Sin(t);

            return new Point3D(x, y, z);
        }

        private static Vector3D GetNormal(double t, double y)
        {
            return (Vector3D)GetPosition(t, y);
        }

        private static Point GetTextureCoordinate(double t, double y)
        {
            Matrix TYtoUV = new Matrix();
            TYtoUV.Scale(1 / (2 * Math.PI), -0.5);

            Point p = new Point(t, y);
            p = p * TYtoUV;

            return p;
        }






        public double RotateX
        {
            get { return (double)GetValue(RotateXProperty); }
            set { SetValue(RotateXProperty, value); }
        }

        // Using a DependencyProperty as the backing store for RotateX.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RotateXProperty =
            DependencyProperty.Register("RotateX", typeof(double), typeof(PanoramaView), new PropertyMetadata(0.0));





        public double RotateY
        {
            get { return (double)GetValue(RotateYProperty); }
            set { SetValue(RotateYProperty, value); }
        }

        // Using a DependencyProperty as the backing store for RotateY.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RotateYProperty =
            DependencyProperty.Register("RotateY", typeof(double), typeof(PanoramaView), new PropertyMetadata(0.0));



        private bool _isOnDrag = false;
        private Point _startPoiint = new Point();
        private double _startRotateX = 0.0;
        private double _startRotateY = 0.0;

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            this._isOnDrag = true;
            this._startPoiint = e.GetPosition(this);
            this._startRotateX = this.RotateX;
            this._startRotateY = this.RotateY;
            base.OnMouseLeftButtonDown(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if(this._isOnDrag == true && e.LeftButton == MouseButtonState.Pressed)
            {
                Vector delta = this._startPoiint - e.GetPosition(this);

                this.RotateX = this._startRotateX + (delta.X / this.ActualWidth * 360);
                this.RotateY = this._startRotateY + (delta.Y / this.ActualHeight * 360);
            }
            base.OnMouseMove(e);
        }


        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            _isOnDrag = false;
            base.OnMouseLeftButtonUp(e);
        }
    }
}


'Microsoft > WPF' 카테고리의 다른 글

TextBox 의 내용이 짤릴 경우에만, ToolTip 보이기  (2) 2014.12.26
Draggable Canvas ver 0.9  (1) 2014.11.17
편집용 Canvas 만들기  (0) 2014.11.16
Expanding Canvas 만들기 2  (0) 2014.11.16
격자 배경 그리기  (0) 2014.11.14