CSS effect blog address:
3D shuttle effect? Easily done with CSS - ChokCoco - Blog Garden (cnblogs.com)
UWP effect blog address:
3D shuttle effect? Also achievable with UWP - dino.c - Blog Garden (cnblogs.com)
The experts truly reach the pinnacle.
I really learn a lot from this.
I’ll just have some fun and try it with WPF. The result is not great, but it’s passable.

Compared to CSS and UWP, the classic WPF is relatively complicated for 3D effects. First, you need to create a 3D model, then rotate the X and Y axes, adjust the Z‑axis stretch, and finally set the FOV. The shuttle effect depends on the value of the Z‑axis stretch; in the image it’s 10‑20.
Of course, the hardest part is the seamless loop of the animation.
Let’s do it step by step.
Creating the 3D model
This part can directly use 2D controls – there’s nothing special to study – so it’s a pure 3D model with textures applied as images.
As for creating the 3D model, the key is to start from left to right.
- Positions
A 3D model is made of triangles, so when describing the shape of an object, you should draw triangles as much as possible. Each point is a vertex. If identical vertices exist, choose only one.
Vertices are described in XYZ. When you look at the object from the front, the Z‑axis points toward you (positive). Y‑axis is up (positive), X‑axis is right (positive).

Note: For convenience, it is recommended to define points in the order: top‑left, bottom‑left, bottom‑right, top‑right.

The vertices of this model are defined as P1(x,y,z), P2(x,y,z), P3(x,y,z), P4(x,y,z).
- TriangleIndices
This describes how the vertices form triangles. When you consider a face as the front, remember to define the vertex indices counterclockwise. The vertex set is Positions, containing the coordinate points you entered, starting from index 0.
The back face is clockwise.
- TextureCoordinates
This specifies the positioning order of the texture. It follows a 2D coordinate system, different from the others. That is, the top‑left corner is (0,0), bottom‑right is (1,1), i.e., the usual screen coordinates.
After creating the model, you need to make four of them.

Rotating the 3D model
Now it gets interesting. After creating the four models above, we need to compose them into four faces, i.e., rotation around the Y‑axis and X‑axis, but the directions are different. We can use Transform for rotation.

Looks okay.
Let’s make four images and test.
For easier debugging, we include a progress bar.
Resources:
<MeshGeometry3D x:Key="Rect3D_O">
<MeshGeometry3D.Positions>
-50, 50, 0, -50, -50, 0, 50, -50, 0, 50, 50, 0
</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices> 0 1 2 2 3 0 </MeshGeometry3D.TriangleIndices>
<MeshGeometry3D.TextureCoordinates>
0,0 0,1 1,1 1,0
</MeshGeometry3D.TextureCoordinates>
<MeshGeometry3D.Normals> 0,0,1, 0,0,1, 0,0,1, 0,0,1 </MeshGeometry3D.Normals>
</MeshGeometry3D>
<DiffuseMaterial x:Key="Img">
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="start.jpg" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<Viewport3D x:Name="View3D_2">
<Viewport3D.Camera>
<PerspectiveCamera
FieldOfView="{Binding ElementName=FOV, Path=Value}"
Position="0,0,100"
LookDirection="0,0,-1"
/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="White" />
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
Axis="0 1 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
Axis="1 0 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
Axis="0 1 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
Axis="1 0 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
<Slider
Grid.Row="1"
x:Name="Left_Top_Z"
Minimum="-90"
Value="12"
Maximum="90"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
Grid.Row="2"
x:Name="Right_Bottom_Z"
Minimum="-90"
Value="-12"
Maximum="0"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
Grid.Row="3"
x:Name="ScaleZ_2"
Background="Red"
Minimum="10"
Maximum="100"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
>
<Slider.Style>
<Style TargetType="Slider">
<Style.Triggers>
<Trigger Property="Tag" Value="1">
<Trigger.EnterActions>
<BeginStoryboard >
<Storyboard>
<DoubleAnimation RepeatBehavior="Forever" Storyboard.TargetProperty="Value" From="11" To="20" BeginTime="0:0:0" Duration="0:0:10" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Slider.Style>
</Slider>
<Slider
Grid.Row="4"
x:Name="FOV"
Minimum="0"
Maximum="180"
Value="176"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
Run it and see:

Looks like it’s starting to feel right.
All that’s left is to fine‑tune the animation.
We want a seamless loop – essentially, we need two animation phases that are indistinguishable at their boundaries. Referencing the experts’ basic ideas, we rely on opacity and delayed playback to make the start and end appear seamless.

That means:

Here we will implement this approach purely in XAML.
First, we need to solve the timing problem. When the page loads, we modify some property of an object via animation. That object has a trigger that monitors it and starts a timer, then plays the animation.
To modify the value, we use the ObjectAnimationUsingKeyFrames animation.
For the trigger, we use a data trigger because it supports binding.
The most important part of the whole process is the timeline – when each of the two sets of animations appears and hides.

Final code
<Window.Resources>
<Storyboard
RepeatBehavior="Forever"
Storyboard.TargetName="ScaleZ_2"
Duration="0:0:10"
x:Key="sb"
>
<DoubleAnimation
Storyboard.TargetName="ScaleZ_2"
Storyboard.TargetProperty="Value"
From="11"
To="20"
BeginTime="0:0:0"
Duration="0:0:10"
/>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="View3D_2"
BeginTime="0:0:0"
>
<LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" />
<LinearDoubleKeyFrame KeyTime="0:0:6" Value="1" />
<LinearDoubleKeyFrame KeyTime="0:0:10" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<MeshGeometry3D x:Key="Rect3D_O">
<MeshGeometry3D.Positions>
-50, 50, 0, -50, -50, 0, 50, -50, 0, 50, 50, 0
</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>
0 1 2 2 3 0
</MeshGeometry3D.TriangleIndices>
<MeshGeometry3D.TextureCoordinates>
0,0 0,1 1,1 1,0
</MeshGeometry3D.TextureCoordinates>
<MeshGeometry3D.Normals>
0,0,1, 0,0,1, 0,0,1, 0,0,1
</MeshGeometry3D.Normals>
</MeshGeometry3D>
<DiffuseMaterial x:Key="Img">
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="start.jpg" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</Window.Resources>
<Window.Triggers>
<EventTrigger RoutedEvent="Loaded" SourceName="ScaleZ">
<BeginStoryboard>
<Storyboard
Storyboard.TargetName="ScaleZ"
Storyboard.TargetProperty="Tag"
Duration="0:0:5.1"
>
<ObjectAnimationUsingKeyFrames>
<DiscreteObjectKeyFrame KeyTime="0:0:5">
<DiscreteObjectKeyFrame.Value> 1 </DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
<BeginStoryboard Storyboard="{StaticResource sb}" />
</EventTrigger>
</Window.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Viewport3D x:Name="View3D_1">
<Viewport3D.Style>
<Style TargetType="Viewport3D">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=ScaleZ,Path=Value}" Value="1">
<DataTrigger.EnterActions>
<BeginStoryboard >
<Storyboard>
<DoubleAnimationUsingKeyFrames RepeatBehavior="Forever" Storyboard.TargetProperty="Opacity" BeginTime="0:0:0" Duration="0:0:10">
<LinearDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
<LinearDoubleKeyFrame KeyTime="0:0:5" Value="1"/>
<LinearDoubleKeyFrame KeyTime="0:0:8" Value="1"/>
<LinearDoubleKeyFrame KeyTime="0:0:10" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Viewport3D.Style>
<Viewport3D.Camera>
<PerspectiveCamera
FieldOfView="{Binding ElementName=FOV, Path=Value}"
Position="0,0,100"
LookDirection="0,0,-1"
/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="White" />
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
Axis="0 1 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
Axis="1 0 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
Axis="0 1 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
Axis="1 0 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
<Viewport3D x:Name="View3D_2">
<Viewport3D.Camera>
<PerspectiveCamera
FieldOfView="{Binding ElementName=FOV, Path=Value}"
Position="0,0,100"
LookDirection="0,0,-1"
/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="White" />
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
Axis="0 1 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Left_Top_Z, Path=Value}"
Axis="1 0 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
Axis="0 1 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
<GeometryModel3D
Geometry="{StaticResource Rect3D_O}"
Material="{StaticResource Img}"
BackMaterial="{StaticResource Img}"
>
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D CenterX="0" CenterY="1" CenterZ="0">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="{Binding ElementName=Right_Bottom_Z, Path=Value}"
Axis="1 0 0"
/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D
ScaleZ="{Binding ElementName=ScaleZ_2, Path=Value}"
/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
<Slider
Grid.Row="1"
x:Name="Left_Top_Z"
Minimum="-90"
Value="12"
Maximum="90"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
Grid.Row="2"
x:Name="Right_Bottom_Z"
Minimum="-90"
Value="-12"
Maximum="0"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
Grid.Row="3"
x:Name="ScaleZ"
Background="Red"
Minimum="10"
Maximum="100"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
>
<Slider.Style>
<Style TargetType="Slider">
<Style.Triggers>
<Trigger Property="Tag" Value="1">
<Trigger.EnterActions>
<BeginStoryboard >
<Storyboard>
<DoubleAnimation RepeatBehavior="Forever" Storyboard.TargetProperty="Value" From="11" To="20" BeginTime="0:0:0" Duration="0:0:10" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Slider.Style>
</Slider>
<Slider
Grid.Row="4"
x:Name="FOV"
Minimum="0"
Maximum="180"
Value="176"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<Slider
Grid.Row="5"
x:Name="ScaleZ_2"
Background="Black"
Minimum="10"
Maximum="100"
ToolTip="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value}"
/>
<StackPanel Grid.Row="6" Orientation="Vertical">
<TextBlock>
<Run Text=" View3D_1 opacity" />
<Run Text="{Binding ElementName=View3D_1, Path=Opacity}" />
<Run Text=" View3D_1 value" />
<Run Text="{Binding ElementName=ScaleZ, Path=Value}" />
<Run Text=" Tag value" />
<Run Text="{Binding ElementName=ScaleZ, Path=Tag}" />
<Run Text=" ZIndex value" />
<Run Text="{Binding ElementName=View3D_1, Path=(Panel.ZIndex)}" />
</TextBlock>
<TextBlock>
<Run Text=" View3D_2 opacity" />
<Run Text="{Binding ElementName=View3D_2, Path=Opacity}" />
<Run Text=" View3D_2 value" />
<Run Text="{Binding ElementName=ScaleZ_2, Path=Value}" />
<Run Text=" ZIndex value" />
<Run Text="{Binding ElementName=View3D_2, Path=(Panel.ZIndex)}" />
</TextBlock>
</StackPanel>
</Grid>
Thus, we achieve this effect without any C# code.

I must admit that the transition at time 0 is not perfect – there’s a slight double‑image – so there’s still much to learn.