Stereoskopisches 3D im Web mit Silverlight (oder Flash)

Normalerweise nehmen wir die Welt räumlich war, indem jedes Auge über die Netzhaut ein zweidimensionales Bild ermittelt und dann an das Gehirn sendet. Sofern zwei funktionstüchtige Augen und ein dazu passendes und entsprechend trainiertes Gehirn vorhanden sind, wird daraus ein räumlicher Sinneseindruck. 3D-Filme machen von diesem Verhalten gebrauch, indem auf irgendeine Art und Weise diese zwei Bilder an jeweils ein Auge gesandt werden (Stereoskopie): Für die Aufnahme werden meist spezielle Linsen, zwei Kameras oder die Verschiebung einer Kamera genutzt. Beim Test hilft fertiges Bildmaterial wie Muttyan’s Stereo Galleries.

3D_cameras In McGyver-Manier haben wir aus zwei alten Webcams (mit gruseliger Bildqualität) und einem Reststück Parkettboden eine Art 3D-USB-Kamera gebaut: Wichtig hierbei ist, dass die Linsen möglichst genau 6,5 cm auseinanderstehen, da dies dem menschlichen Seheindruck am nächsten kommt (größer heißt, dass alles kleiner wirkt und bei kleinerem Abstand wirkt alles größer). Nützlich ist übrigens bei dieser Handwerkskunst, dass die Kameras zueinander gedreht werden können, um einen gemeinsamen Punkt im Raum besser zu fokussieren (so wie das unsere Augen ja auch machen): Dies hilft ein wenig bei der Optimierung des Bildes. Die Qualität einer solchen Lösung ist trotzdem nicht vergleichbar mit den Verfahren bei modernen Filmen wie Avatar und darunter leidet auch die räumliche Wahrnehmung. Bei Avatar wurde übrigens mit zwei beweglichen Objektiven/Kameras im 6,5 cm Abstand gearbeitet, die sich zueinander wie das menschliche Auge drehen und so Punkte fokussieren können.

Es gibt eine Reihe von Verfahren, um dem rechten und linken Auge jeweils unterschiedliche Bilder zu präsentieren. Günstig und qualitativ halbwegs erträglich ist die Farbanaglyphentechnik. Dabei werden das rechte und linke Bild übereinander projiziert und jeweils eingefärbt (oft in Komplementärfarben wie Rot und Grün). Über eine spezielle Brille mit Farbfiltern wird auf jedem Auge dann ein Bild ausgelöscht. Heutzutage gängig ist die Kombination Rot und Cyan, die sich auch für Werbemaßnahmen und Comics eignet (entsprechende Brillen gibt es günstig bei Ebay). Da dabei ohnehin viele Farbinformationen verloren gehen, tendieren viele Produzenten zu Schwarzweiß – außerdem neigt diese Technik zu Geisterbildern, wenn Einfärbung und Farbfilter nicht ganz genau übereinstimmen. Aufwendigere Verfahren wie die Polarisationstechnik helfen die Geisterbilder zu vermeiden und arbeiten auch gut mit farbigen Inhalten. Das Einfärben der Inhalte gelingt mit Bildbearbeitungsprogrammen (es gibt auch darauf spezialisierte Programme) oder im Falle von Flash und Silverlight auch zur Laufzeit über Pixelshader. Bei Flash kommt dafür die Sprache Hydra und das Werkzeug Pixelbender zum Einsatz, Silverlight nutzt HSHL (High Level Shading Language) und das DirectX SDK. Eine Erleichterung ist die kostenlose Click-Once Anwendung Shazzam.

Der Folgende Effekt namens ColorChannelsEffect kümmert sich um die Umwandlung eines Bildes in Graustufen und das Variieren der Kanäle für Rot, Grün, Blau und die Transparenz (Alpha). Shazzam erzeugt automatisch den benötigten Code, so dass der Effekt dann direkt in Silverlight (oder auch WPF) genutzt werden kann:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// a shader is an algorithm that is compiled
// and loaded into the Graphics Processor Unit (GPU)
// this algorithm is run once, for every pixel in the input image
// GPUs are efficient parallel  processors and will run
// your algorithm on thousands of pixels at a time
 
/// <class>ColorChannelsEffect</class>
/// <description>An effect that turns the input into a grayscale image first and than multiplies all four channels with custom values (e.g. for anaglyph images).</description>
 
//-----------------------------------------------------------------------------------------
// Shader constant register mappings (scalars - float, double, Point, Color, Point3D, etc.)
//-----------------------------------------------------------------------------------------
 
/// <summary>Change the ratio between the Red channel</summary>
/// <minvalue>0/minValue&gt;
/// <maxvalue>1</maxvalue>
/// <defaultvalue>1</defaultvalue>
float RedRatio : register(c0);
 
/// <summary>Change the ratio between the Blue channel </summary>
/// <minvalue>0/minValue&gt;
/// <maxvalue>1</maxvalue>
/// <defaultvalue>1</defaultvalue>
float BlueRatio : register(c1);
 
/// <summary>Change the ratio between the Green channel</summary>
/// <minvalue>0/minValue&gt;
/// <maxvalue>1</maxvalue>
/// <defaultvalue>1</defaultvalue>
float GreenRatio : register(c2);
 
/// <summary>Change the ratio between the Alpha channel</summary>
/// <minvalue>0/minValue&gt;
/// <maxvalue>1</maxvalue>
/// <defaultvalue>1</defaultvalue>
float AlphaRatio : register(c3);
 
// input image
sampler2D input : register(s0);
 
// this is the entry point for the shader
// the return type is float4
// float4 contains four float values which we 
// can think of as color of a pixel
// (alpha, red, green, blue)
float4 main(float2 locationInSource : TEXCOORD) : COLOR
{
 
  // create a variable to hold our color
  float4 color;
 
  // get the color of the current pixel
  // tex2D is a HLSL function
  // 1st arg is a bitmap (called a texture in HLSL)
  // input: the incoming image, passed in from the GPU register (s0) 
  // 
  // 2nd arg is a locator for the pixel, this is normalized to range 0..1 
 
  // tex2D takes our sample input, gets a pixel at the current x,y location
  // and returns the color of the existing pixel, which means that the color is not altered
  color = tex2D( input, locationInSource.xy);
 
  // color has four value that we can change
  // color.r, color.g, color.b, color.a
 
  // get rgb values only
  float3 rgb = color.rgb;
  // create greyscale pixel via dot multiplication
  // (the effective luminance of a pixel is calculated 
  // with the following formula: 
  // 0.3 * red + 0.59 * green + 0.11 * blue);
  float3 luminance = dot(rgb, float3(0.30, 0.59, 0.11));
  // convert rgb to four channel pixel including alpha 
  color = float4(luminance, color.a);
 
  // values are normalized, so 0 is no color, 1 is full color
  color.r= color.r * RedRatio;
  color.g= color.g * GreenRatio;
  color.b= color.b * BlueRatio;
  color.a= color.a * AlphaRatio;
 
  return color;
 
}

Für die Nutzung dieses Effekts müssen die Klassendatei für ColorFiltersEffect.cs (bzw. das VisualBasic-Pendant) in das Projekt kopiert und der ColorFiltersEffect.ps als Ressource eingebunden werden (ColorFiltersEffect.fx ist nur der Quellcode des Filters und im Silverlight-Projekt nicht erforderlich). Ein kleiner Tipp am Rande: In der ColorFiltersEffect.cs-Datei muss vermutlich die pixelShader.UriSource angepasst werden: Das Beispielprojekt nutzt den Namensraum und den Assembly-Namen StereoscopeImage und der Effekt liegt dort im Verzeichnis Effects, so dass die Anweisung mit der UriSource dann so aussieht:

1
pixelShader.UriSource = new Uri("/StereoscopeImage;component/Effects/ColorChannelsEffect.ps", UriKind.Relative);

Die eigentliche Anwendung besteht aus zwei Bildern, die über die Effekte jeweils unterschiedlich eingefärbt werden. Da eine Rot-Cyan-Brille zum Einsatz kommt, müssen für das linke Auge die Kanäle Grün und Blau und für das rechte Auge die Kanäle Rot und Alpha ausgeblendet werden. Bitte beachten Sie, dass die Bilder im Beispiel nur per URL referenziert werden und das Projekt darum als OOB-Anwendung mit erweiterten Rechten läuft – natürlich können Sie auch andere Bilder verwenden und direkt in das Projekt einbinden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<UserControl x:Class="StereoscopeImage.MainPage"
    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"
    xmlns:effects="clr-namespace:Biz.Wolter.Effects"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
 
    <Grid x:Name="LayoutRoot" Background="White">
        <!-- 
            Due to legal issues I did not include the original image. 
            You can either use the URL in a trusted OOB application 
            or just download free images yourself.
        -->
        <Image x:Name="leftImage" Source="http://www.stereomaker.net/galleries/wien/lr/20090921011759_l.jpg" >
            <Image.Effect>
                <effects:ColorChannelsEffect RedRatio="1" GreenRatio="0" BlueRatio="0" AlphaRatio="1" />
            </Image.Effect>
        </Image>
        <Image x:Name="rightImage" Source="http://www.stereomaker.net/galleries/wien/lr/20090921011759_r.jpg" >
            <Image.Effect>
                <effects:ColorChannelsEffect RedRatio="0" GreenRatio="1" BlueRatio="1" AlphaRatio="0" />
            </Image.Effect>
        </Image>
    </Grid>
 
</UserControl>

Das war es dann schon mit Schritt eins. Sollte das funktionieren, dann kann nun der Versuch unternommen werden, Bewegung ins Spiel zu bringen: In diesem Fall über zwei Kameras, die das linke und rechte Bild aufzeichnen.
Dafür benötigen wir anstelle der Bilder zwei Rechtecke als Container für einen VideoBrush (dieser stellt dann das Video dar) sowie ein paar Bedienelemente zum Auswählen der Kameras und zum start der Aufzeichnung.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<UserControl x:Class="StereoscopeVideo.MainPage"
    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"
    xmlns:effects="clr-namespace:Biz.Wolter.Effects"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
 
    <Grid x:Name="LayoutRoot" Background="White">
 
        <Rectangle x:Name="leftImage" Stroke="Black">
            <Rectangle.Effect>
                <effects:ColorChannelsEffect RedRatio="1" GreenRatio="0" BlueRatio="0" AlphaRatio="1" />
            </Rectangle.Effect>
        </Rectangle>
        <Rectangle x:Name="rightImage" Stroke="Black">
            <Rectangle.Effect>
                <effects:ColorChannelsEffect RedRatio="0" GreenRatio="1" BlueRatio="1" AlphaRatio="0" />
            </Rectangle.Effect>
        </Rectangle>
 
        <ComboBox DisplayMemberPath="FriendlyName" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="leftDeviceComboBox" VerticalAlignment="Top" Width="120" />
        <ComboBox DisplayMemberPath="FriendlyName" Height="23" HorizontalAlignment="Left" Margin="12,41,0,0" Name="rightDeviceComboBox" VerticalAlignment="Top" Width="120" />
        <Button Content="Start" Height="23" HorizontalAlignment="Left" Margin="12,70,0,0" Name="startButton" VerticalAlignment="Top" Width="75" Click="startButton_Click" />
 
    </Grid>
 
</UserControl>

Innerhalb der Code-Behind-Datei werden die VideoBrushes mit dem Video-Signal befüllt, sobald der Anwender die Start-Schaltfläche klickt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Collections.ObjectModel;
 
namespace StereoscopeVideo
{
    public partial class MainPage : UserControl
    {
 
        private CaptureSource leftSource;
        private CaptureSource rightSource;
        private VideoBrush leftVideoBrush;
        private VideoBrush rightVideoBrush;
 
        public MainPage()
        {
            InitializeComponent();
 
            Loaded += new RoutedEventHandler(mainPage_Loaded);
        }
 
        private void mainPage_Loaded(object sender, RoutedEventArgs e)
        {
            ReadOnlyCollection<VideoCaptureDevice> devices = CaptureDeviceConfiguration.GetAvailableVideoCaptureDevices();
 
            // set left device collection
            leftDeviceComboBox.ItemsSource = devices;
            // create left capture source (capturing the video)
            leftSource = new CaptureSource();
            // create left video brush (displaying the video)...
            leftVideoBrush = new VideoBrush();
            leftVideoBrush.Stretch = Stretch.Uniform;
            // ...and use this brush to fill the corresponding ui-control
            leftImage.Fill = leftVideoBrush;
 
            rightDeviceComboBox.ItemsSource = devices;
            rightSource = new CaptureSource();
            rightVideoBrush = new VideoBrush();
            rightVideoBrush.Stretch = Stretch.Uniform;
            rightImage.Fill = rightVideoBrush;
        }
 
        private void startButton_Click(object sender, RoutedEventArgs e)
        {
            if (leftDeviceComboBox.SelectedIndex > -1 && rightDeviceComboBox.SelectedIndex > -1)
            {
                startButton.IsEnabled = false; // for testing purpose only one start possble so far
                startCapturing();
            }
            else
            {
                MessageBox.Show("Please choose two input devices first!", "Alert", MessageBoxButton.OK);
            }
        }
 
        private void startCapturing()
        {
 
            leftSource.VideoCaptureDevice = leftDeviceComboBox.SelectedItem as VideoCaptureDevice;
            rightSource.VideoCaptureDevice = rightDeviceComboBox.SelectedItem as VideoCaptureDevice;
 
            if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess())
            {
                leftSource.Start();
                leftVideoBrush.SetSource(leftSource);
 
                if (leftSource.VideoCaptureDevice != rightSource.VideoCaptureDevice)
                {
                    rightSource.Start();
                    rightVideoBrush.SetSource(rightSource);
                }
                else
                {
                    MessageBox.Show("Only one innput device does not really make sense for this 3D application!", "Alert", MessageBoxButton.OK);
                    rightVideoBrush.SetSource(leftSource);
                }
            }
 
        }
    }
}

Das war es schon. Beide Silverlight-4-Projekte gibt es hier für Visual Studio 2010 oder Expression Blend 4 als ZIP zum Download. Viel Spaß damit…
Das Projekt ist nun bei Codeplex gehostet. Die aktuellen Quellen für Visual Studio 2010 oder Expression Blend 4 finden sich unter http://stereoscopy.codeplex.com/. Viel Spaß damit…

3 thoughts on “Stereoskopisches 3D im Web mit Silverlight (oder Flash)

  1. Ups, habe einen kleinen Tippfehler im Code. Es muss natürlich Stereoscopy und nicht Stereoscope heißen. Es sollte aber trotzdem alles funktionieren ;).

Comments are closed.