Balder 3D

Essa semana lá no C.E.S.A.R, nós precisamos iniciar a migração de uma aplicação em WPF para Silverlight. Essa aplicação utiliza a API 3D do WPF, por isso tivemos que buscar uma alternativa para termos os mesmos recursos em Silverlight. Foi então que encontramos o Balder.

O Balder é um projeto opensource hospedado no Codeplex e que possui licença Ms-PL, salvo o caso de se querer comercializar uma API 3D. Ele foi desenvolvido para rodar no Silverlight, WPF e Windows Phone 7. Isso está nos dando uma grande flexibilidade.

Os principais elementos para criar uma cena com Balder são:

  • Game: clr-namespace:Balder.Execution;assembly=Balder
  • Camera: clr-namespace:Balder.View;assembly=Balder
  • Light: clr-namespace:Balder.Lighting;assembly=Balder
  • Geometria: clr-namespace:Balder.Objects.Geometries;assembly=Balder
  • Material: clr-namespace:Balder.Materials;assembly=Balder

Abaixo segue um exemplo com adicionar os namespaces e utilizar esses elementos básicos:

<UserControl x:Class="BalderLab.SimpleExample"
             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:execution="clr-namespace:Balder.Execution;assembly=Balder"
             xmlns:geometries="clr-namespace:Balder.Objects.Geometries;assembly=Balder"
             xmlns:view="clr-namespace:Balder.View;assembly=Balder"
             xmlns:lighting="clr-namespace:Balder.Lighting;assembly=Balder"
             xmlns:material="clr-namespace:Balder.Materials;assembly=Balder">
    <Grid x:Name="LayoutRoot" Background="White">
        <execution:Game Width="640" Height="480">
            <execution:Game.Camera>
                <view:Camera Position="0,5,-10" />
            </execution:Game.Camera>
            <lighting:OmniLight Position="0,10,-10" Diffuse="White" />
            <geometries:Box Dimension="4,4,4" Position="0,0,0">
                <geometries:Box.Material>
                    <material:Material Ambient="White" Diffuse="Red" Specular="LightGreen" Shade="Gouraud" />
                </geometries:Box.Material>
            </geometries:Box>
        </execution:Game>
    </Grid>
</UserControl>

Sempre que adicionamos algum elemento 3D na tela, nossa maior vontade é de poder interagir com ele. Para rotacionar um elemento na tela, o Balder oferece um propriedade chamada InteractionEnabled. Caso você queira compor objetos separados para que eles se comportem como um, pode usar um Container conforme ilustrado abaixo.

<execution:Game Width="640" Height="480">
    <execution:Game.Camera>
        <view:Camera Position="0,5,-10" />
    </execution:Game.Camera>
    <lighting:OmniLight Position="0,10,-10" Diffuse="White" />
    <balder:Container InteractionEnabled="True">
        <geometries:ChamferBox Dimension="2,2,2">
            <geometries:Box.Material>
                <material:Material Ambient="White" Diffuse="Green" Specular="LightGreen" Shade="Gouraud" />
            </geometries:Box.Material>
        </geometries:ChamferBox>
        <geometries:Box Dimension="2,2,2" Position="4,0,0">
            <geometries:Box.Material>
                <material:Material Ambient="White" Diffuse="Red" Specular="LightGreen" Shade="Gouraud" />
            </geometries:Box.Material>
        </geometries:Box>
    </balder:Container>
</execution:Game>

O Balder não oferece nativamente um recurso de zoom, no entando ele pode ser facilmente implementado programaticamente alterando as propriedades da câmera. Esse exemplo pode ser encontrado no próprio site de samples do projeto.

No caso de querer trabalhar com modelos 3D mais complexos, a forma mais fácil é carregar os arquivos no formato .ASE (ASCII Scene Exporter). Até onde sei, esse formato pode ser exportado a partir do 3ds Max e existe um plugin para o Blender 3D.

<Geometries:Mesh AssetName="/MyGameApp;component/Assets/teapot.ase"/>

Para ter uma introdução rápida sobre o Balder em vídeo, veja esse link. Minha advertência sobre esse vídeo é que os valores default da versão atual são diferentes da versão utilizada pelo vídeo.

Anúncios

Sincronizar scroll do ListView no WPF

Vou mostrar a forma que encontrei para solucionar o problema de como sincronizar as barras de rolagem de dois LisViews, embora esse mesmo código possa ser adaptado para qualquer componente que use scrolls. No C.E.S.A.R, usamos essa técnica para simular o congelamento de colunas, recurso ainda não suportado nativamente pelo WPF. Esse não é um caso real, mantive o mais simples possível para facilitar o entendimento.

No código abaixo podemos perceber dois ListViews posicionados usando Grid: um a esquerda e outro a direita. O que quero mostrar é o efeito de quando o usuário usar a barra de rolagem no lado direito, a barra do lado esquerdo também irá se movimentar na mesma proporção.

<Window x:Class="ScrollDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:models="clr-namespace:ScrollDemo.Models"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <models:People x:Key="people" />
        </Grid.Resources>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <ListView x:Name="listView" 
                  ItemsSource="{StaticResource ResourceKey=people}">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Id" 
                                        DisplayMemberBinding="{Binding Id}" />
                        <GridViewColumn Header="Name"  
                                        DisplayMemberBinding="{Binding Name}" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
        <ListView ItemsSource="{StaticResource ResourceKey=people}" Grid.Column="1">
            <ListView.Resources>
                <Style TargetType="ScrollBar">
                    <EventSetter Event="ValueChanged" 
                                 Handler="ScrollValueChanged" />
                </Style>
            </ListView.Resources>
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Phone" 
                                        DisplayMemberBinding="{Binding Phone}" />
                        <GridViewColumn Header="Address" 
                                        DisplayMemberBinding="{Binding Address}" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

Para conseguir capturar o evento, usamos o recurso de estilo com um target para ScrollBar dentro do ListView. O que as linhas 28 a 33 estão dizendo é: para qualquer ScrollBar dentro desse ListView, dispare o evento ValueChanged chamando o método ScrollValueChanged. O método ScrollValueChanged está ilustrado abaixo.

protected void ScrollValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    ScrollBar source = sender as ScrollBar;
    if (source == null || source.Orientation == Orientation.Horizontal)
        return;

    ScrollViewer target = GetScrollViewer(this.listView) as ScrollViewer;
    if (target == null)
        return;

    target.ScrollToVerticalOffset(source.Value);
}

O grande segredo desse evento está no método GetScrollViewer.

public static DependencyObject GetScrollViewer(DependencyObject container)
{
    if (container is ScrollViewer)
        return container;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(container); i++)
    {
        var child = VisualTreeHelper.GetChild(container, i);
        var result = GetScrollViewer(child);
        if (result != null)
            return result;
    }

    return null;
}

%d blogueiros gostam disto: