在WPF中使用PlaneProjection模拟动态3D效果

虽然在 WPF 中也集成了 3D 呈现的功能,在简单的 3D 应用中,有时候并不需要真实光影的 3D 场景。毕竟使用 3D 引擎会消耗很多资源,有时候使用各种变换和假的阴影贴图也能设计出既省资源,又有很好用户体验的“伪”3D 界面。

在 Silverlight 中,因为性能问题,一般并不使用真 3D 引擎,微软为 Silverlight 提供了 System.Windows.Media.PlaneProjection 类,用投影变换来模拟 3D 的效果。

下面让我们看下一个 Microsoft Expression Blend 4 提供的示例 Wall3D (位于帮助>欢迎屏幕>示例)。

img

大家不要被这个可以流畅滚动的 3D 图片墙所迷惑,其实这只是一个 ListBox 控件。MainPage 中给 ListBox 定义了一个 ItemsPanelTemplate,使用新的控件来作为 ListBox 中 Items 的布局控件,这个控件就是这个项目最核心的类:CircularPanel3D。

CircularPanel3D 类继承自 System.Windows.Controls.Panel,它实现了一种新的布局方式,效果大家在上一张图片中都看到了。这种华丽的效果实际上都是由这个最重要的类中的最重要的方法: private void Refresh() 完成的。

private void Refresh()
{
    //几个计数器,看名字就功能很明了
    int count = 0;
    int col = 0;
    int row = 0;
    int zLevel = 0;

    //开始遍历子元素
    foreach (FrameworkElement childElement in this.Children)
    {
        //AngleItem是指单个元素的旋转角度,算法是360除以列数
        //这个方法的布局方式是先布满一圈,再下一环的,角度总是可以取模的
        //所以这边直接AngleItem和count相乘了
        //InitialAngle这个属性是用来确定整个圆环的偏转角度的,每次这个依赖属性变化就会重新计算布局(调用这个方法)
        double angle = (this.AngleItem * count++) - this.InitialAngle;
        //下面两个变量用来确定元素在屏幕上的位置,用到了三角函数,数学不好的请问高中数学老师
        double x = this.Radius * Math.Cos(Math.PI * angle / 180);
        double z = this.Radius * Math.Sin(Math.PI * angle / 180);
        //创建个PlaneProjection对象,并赋值
        PlaneProjection projection = new PlaneProjection();
        if (projection != null)
        {
            projection.CenterOfRotationX = 0.5;
            projection.CenterOfRotationY = 0.5;
            projection.CenterOfRotationZ = 0.5;
            projection.RotationY = angle + 90;
            projection.GlobalOffsetX = x;
            //Distance实际上就是模拟的镜头距离
            projection.GlobalOffsetZ = z - this.Distance;
            //-330。。。坑爹的硬编码,实际上就是两行元素的间距,OffsetY是纵向的偏移量,用于调整环在屏幕上的位置
            projection.GlobalOffsetY = row * (-330) + this.OffsetY;
        }
        //实际上是让double数变成int数,但是又不会丧失区别性,下面要用到
        int depth = (int)(z * 100);

        double pDist = (this.Distance - 1000) / 2000;
        double pZ = ((z + 1000) / 2000) + 0.5;

        //让太远的和太近的变透明
        double opacity = (pZ - pDist) + 0.4;
        if (opacity >= 1)
        {
            childElement.Opacity = (2 - opacity);
        }
        else if (opacity < 0)
        {
            childElement.Opacity = 0;
        }
        else
        {
            childElement.Opacity = opacity;
        }

        // 嗯这边有原版的英文注释,不解释
        // Variable zLevel changes value of ZIndex for each item in the ListBox.
        // This way the reflex of elements at the top will be placed behind the item below it.
        Canvas.SetZIndex(childElement, depth - (++zLevel * 10));

        //根据Align属性设置对齐方式,不是很重要
        double alignX = 0;
        double alignY = 0;
        switch (this.Align)
        {
            case AlignmentOptions.Left:
                alignX = 0;
                alignY = 0;
                break;
            case AlignmentOptions.Center:
                alignX = childElement.DesiredSize.Width / 2;
                alignY = childElement.DesiredSize.Height / 2;
                break;
            case AlignmentOptions.Right:
                alignX = childElement.DesiredSize.Width;
                alignY = childElement.DesiredSize.Height;
                break;
        }
        //将PlaneProjection对象赋给子元素的Projection属性
        childElement.Projection = projection;
        //定位子元素
        childElement.Arrange(new Rect(this.Width / 2 - alignX, this.Height / 2 - alignY, childElement.DesiredSize.Width, childElement.DesiredSize.Height));

        //换行,又见坑爹的硬编码14。。这个代表有十四列
        col++;
        if (col > 14)
        {
            col = 0;
            row++;
        }
    }
}