0

I’m trying to implement a “tilt” effect (a slight 3D tilt of an element on press/hover) in pure WPF, similar to UWP/WinUI. I’m using the classic approach with Viewport2DVisual3D (Greg Schechter’s Planerator). However, when the visual has a DropShadowEffect applied, the content appears visually “deformed”/shifted relative to the expected center during rotation.

I’d like to understand how to do this correctly in WPF—either how to use DropShadow so the tilt works without deformation, or how to properly compute bounds/camera, or how to separate the shadow.


Environment

  • WPF (.NET 6/7/8), Windows 10/11
  • Effect: custom TiltEffect (on press/hover, wraps the element in a Planerator and animates RotationX/RotationY/Depth)
  • 3D mapping: Viewport2DVisual3D (Planerator)
  • Elements have transparent backgrounds; shadow is via DropShadowEffect (or an equivalent blurred shadow)

What happens

  • If I apply a DropShadowEffect directly on the element that’s mapped into 3D (it’s the Viewport2DVisual3D.Visual), WPF first renders the element into an off-screen texture with expanded bounds for the blur/shadow and only then projects it into 3D.
  • This changes the “optical” size and the offset relative to RenderSize. As a result, during rotation/tilt, the center shifts and the whole thing looks like a deformation/shift of the content.

See screenshots:

  • [before tilt] – content without rotation (as expected)
  • [during tilt] – with DropShadowEffect, you can see the shift and “odd” skew when rotated

(2 images: “before.png”, “tilted.png”)

enter image description here enter image description here


Minimal Repro

A shortened example—a button in a container to which I apply shadow and tilt. The tilt code toggles an attached IsPressed; Planerator projects everything into 3D.

<local:DropShadowPanel
    x:Name="Card"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    tiltEffectAnimation:TiltEffect.IsEnabled="True"
    tiltEffectAnimation:TiltEffect.TiltFactor="7"
    BlurRadius="20"
    CornerRadius="10"
    Direction="270"
    RenderingBias="Performance"
    ShadowDepth="0"
    ShadowMode="Outer"
    ShadowOpacity="0.8"
    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
    UseLayoutRounding="{TemplateBinding UseLayoutRounding}"
    Color="Black">
    <Border
        x:Name="PART_BorderContent"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"
        Background="{TemplateBinding Background}"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}"
        CornerRadius="4"
        RenderTransformOrigin="0.5,0.5">
        <ContentPresenter
            Margin="{TemplateBinding Padding}"
            HorizontalAlignment="Center"
            VerticalAlignment="Center" />
    </Border>
</local:DropShadowPanel>
<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="CommonStates">
        <VisualState x:Name="Normal">
            <Storyboard>
                <tiltEffectAnimation:PointerUpThemeAnimation TargetName="Card" />
            </Storyboard>
        </VisualState>
        <VisualState x:Name="MouseOver">
            <Storyboard>
                <tiltEffectAnimation:PointerUpThemeAnimation TargetName="Card" />
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Pressed">
            <Storyboard>
                <tiltEffectAnimation:PointerDownThemeAnimation TargetName="Card" />
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!-- resources -->
<DropShadowEffect x:Key="Shadow" BlurRadius="18" ShadowDepth="6" Color="Black" Opacity="0.28" />
// The events only toggle IsPressed; TiltEffect then sets PlaceIn3D=true internally and animates the rotations
private void OnDown(object sender, MouseButtonEventArgs e) => TiltEffect.SetIsPressed((FrameworkElement)sender, true);
private void OnUp(object sender, RoutedEventArgs e)       => TiltEffect.SetIsPressed((FrameworkElement)sender, false);

Planerator (from Greg Schechter) maps 2D into 3D and works great in most examples. In my case, I’ve already accounted for “neutral” bounds, because the shadow/clip can shift the origin into negative coordinates:

// key part: Update3D in the Planerator
private void Update3D()
{
    if (_logicalChild == null) return;

    Rect bounds = VisualTreeHelper.GetDescendantBounds(_logicalChild);
    double w = _logicalChild.RenderSize.Width;
    double h = _logicalChild.RenderSize.Height;

    if (w <= 0 || h <= 0)
    {
        w = bounds.Width;
        h = bounds.Height;
    }

    double fovInRadians = FieldOfView * (Math.PI / 180);
    double zValue = w / Math.Tan(fovInRadians / 2) / 2;

    _viewport3d.Camera = new PerspectiveCamera(
        new Point3D(w / 2, h / 2, zValue),
        new Vector3D(0, 0, -1),
        new Vector3D(0, 1, 0),
        FieldOfView);

    // Compensate for bounds offset (shadow/clip can shift the origin)
    _translateTransform.OffsetX = -bounds.X;
    _translateTransform.OffsetY = -bounds.Y;

    _scaleTransform.ScaleX = w;
    _scaleTransform.ScaleY = h;

    _rotationTransform.CenterX = w * OriginX;
    _rotationTransform.CenterY = h * OriginY;
}

Even with this compensation, having a DropShadowEffect on the “front” visual still shows a visible shift/deformation during rotation.


What I’ve tried

  1. Don’t apply DropShadowEffect to the content; instead render the shadow as a separate layer below the content (e.g. a Border with the same CornerRadius + BlurEffect/DropShadowEffect, and the content without an effect).
    – Result: if the shadow is part of the same visual mapped into 3D, the issue appears (to varying degrees). If I keep the shadow outside the 3D child (i.e., the shadow is “2D behind the scene” and does not tilt), the tilt is clean, but the shadow doesn’t go through perspective.

  2. Caching (RenderOptions.SetCachingHint, BitmapCache) – no significant effect.

  3. Compute everything from GetDescendantBounds and subtract bounds.X/Y (see above) – better, but still not 100% in all cases.

  4. Explicit ClipToBounds, various SnapsToDevicePixels, UseLayoutRounding – no complete fix.


Hypothesis

This is due to how WPF handles effects and Viewport2DVisual3D:

  • DropShadowEffect (like other Effects) renders the 2D visual to an off-screen render target (texture) and expands its bounds for the blur/shadow.
  • Viewport2DVisual3D then works with this “post-effect” texture, but the size/origin may not match what I expect from RenderSize or even GetDescendantBounds in various combinations (transparent background, corners, clips).
  • The result is that the rotation center and projection don’t correspond to the “visual” center of the content.

This matches the observation (paraphrased):
“When you apply DropShadowEffect directly to an element that is then mapped into 3D (Viewport2DVisual3D), WPF first renders the element into a texture with expanded bounds and only then projects it. This changes the visual dimensions and offset relative to RenderSize, which manifests as deformation during rotation.”


Questions

  • Is there a “correct” way in pure WPF to use DropShadowEffect on an element that is the visual for Viewport2DVisual3D so that tilt/rotation doesn’t lead to shifts/deformations?
  • Can we reliably get the “post-effect” bounds (i.e., what actually gets mapped into 3D) and use those for the camera/rotation centers? Is there an API for “render bounds” after applying Effect (something like GetRenderBounds)?
  • Is the recommended solution to fully separate the shadow:
    • a) don’t map it into 3D (it remains 2D and doesn’t tilt), or
    • b) create a custom “shadow layer” inside the same 3D surface but without using Effect (e.g., manually drawn blurred shape), to avoid WPF’s off-screen render of effects?
  • Is this a known limitation/bug of Viewport2DVisual3D + Effect, or can DropShadowEffect be configured so that it doesn’t affect the mapping (e.g., not expanding layout/render bounds)?

Notes on the MRE

  • I’m using Planerator (variant from Greg Schechter’s blog); the front face is a Viewport2DVisual3D with my visual, the back face is a VisualBrush with the same logical child.
  • When TiltEffect.IsPressed=true is toggled, the element is wrapped in the Planerator and RotationX/RotationY/Depth are animated.
  • If I remove DropShadowEffect (or keep the shadow outside the visual that’s mapped into 3D), the tilt looks correct.

If needed, I can attach a full MRE (Planerator + TiltEffect), but the snippets and description above should be enough to reproduce.


Goal

Find a stable and idiomatic solution in pure WPF:

  • either how to use the effect/shadow so tilt works without shifts/deformations,
  • or how to correctly compute/normalize bounds and rotation centers for Viewport2DVisual3D in the presence of effects,
  • or confirm that the only robust approach is to fully separate the shadow from the 3D mapping (and if it needs to tilt, “simulate” it without Effect directly in the drawing).

Source code is on GitHub: https://github.com/ORRNY/Wpf-DropShadowPanel-TiltEffect

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.