C# 实现多种形式的3D翻转页面效果
任务背景
实现一个类似 PPT 或 WPF 中的 3D 翻转效果,比如下面是从PPT录制的效果(WPS中的PPT翻页效果之一):

需要把一张图片分成 M×N 的网格,每个小方块像卡片一样绕 Y 轴旋转,有透视效果,呈现真实的 3D 空间感。看起来很简单对吧?我也这么想,因为WPF里面实现一个3D翻转就非常简单的几句代码就能搞定,区别就是我们要实现更多小方块的3D翻转,比如:
<Grid>
<!-- 带 3D 投影的元素 -->
<Border x:Name="card" Background="DodgerBlue" Width="200" Height="150"
CornerRadius="10" BorderBrush="White" BorderThickness="2">
<Border.Projection>
<PlaneProjection x:Name="projection" RotationY="0"/>
</Border.Projection>
<TextBlock Text="点击翻转" FontSize="24" Foreground="White"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
<Window.Resources>
<!-- 翻转动画 -->
<Storyboard x:Key="FlipAnimation">
<DoubleAnimation Storyboard.TargetName="projection"
Storyboard.TargetProperty="RotationY"
From="0" To="180" Duration="0:0:0.6">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Window.Resources>
但是使用着色器实现这个效果,却需要从头起步,这可把我难坏了,

反复的调整,也出现了五八门的错误,而且好像越调整越乱套的感觉:

终于调整好了:
我调整了效果的变换实际,让整体呈现从左到右的渐变感,因此越向右列时间延迟越大;并且让同列的行也呈现有序感,不同行递增时间延迟,整体呈现出来一种波浪推进的感觉,很酷。

然后基于正确的效果,我进行了方块数量的调整:

然后又进行了方块随机延迟的调整:

然后又进行了多图切换的调整:

以上的一系列的效果都非常棒。部分算法的核心代码(DirectX 12 + ComputeSharp + Win32 ):
public float4 Execute()
{
int2 xy = ThreadIds.XY;
float2 resolution = (float2)DispatchSize.XY;
float2 uv = ((float2)xy + 0.5f) / resolution;
// 图像适配屏幕
float imageAspect = imageWidth / imageHeight;
float screenAspect = screenWidth / screenHeight;
float2 sampleUv = uv;
if (imageAspect > screenAspect)
{
sampleUv.Y = (uv.Y - 0.5f) * (screenAspect / imageAspect) + 0.5f;
}
else
{
sampleUv.X = (uv.X - 0.5f) * (imageAspect / screenAspect) + 0.5f;
}
sampleUv = Hlsl.Clamp(sampleUv, 0f, 1f);
// 网格划分
float frows = (float)rows;
float fcols = (float)cols;
float cellY = sampleUv.Y * frows;
float cellX = sampleUv.X * fcols;
int row = (int)Hlsl.Floor(cellY);
int col = (int)Hlsl.Floor(cellX);
row = Hlsl.Clamp(row, 0, rows - 1);
col = Hlsl.Clamp(col, 0, cols - 1);
float localX = Hlsl.Frac(cellX);
float localY = Hlsl.Frac(cellY);
// 从左到右按列延迟 + 行随机延迟
float colFactor = (float)col / Hlsl.Max(fcols - 1f, 1f);
float colDelay = colFactor * ColumnDelayRatio;
float rowRandomDelay = Hash((float)row, (float)col) * RowRandomDelayRange;
float delay = colDelay + rowRandomDelay;
float span = Hlsl.Max(1f - ColumnDelayRatio - RowRandomDelayRange, 0.2f);
float progressCell = Hlsl.Saturate((progress - delay) / span);
// === 关键:两图切换的角度计算(无透明度变化) ===
//
// progressCell: 0 → 0.5 → 1
//
// 前半部分(progressCell < 0.5):显示 A 图,从 0° 转到 90°(正面→侧面)
// 后半部分(progressCell >= 0.5):显示 B 图,从 90° 转到 0°(侧面→正面)
bool showImageA = progressCell < 0.5f;
float angle;
if (showImageA)
{
// A 图:0° → 90°(正面翻转到侧面)
float t = progressCell * 2f; // 0 → 1
angle = t * PiOver2; // 0° → 90°
}
else
{
// B 图:90° → 0°(侧面翻转到正面)
float t = (progressCell - 0.5f) * 2f; // 0 → 1
angle = PiOver2 - t * PiOver2; // 90° → 0°
}
float cosA = Hlsl.Cos(angle);
float sinA = Hlsl.Sin(angle);
// 当角度接近 90° 时,cos 接近 0,方块几乎不可见
if (cosA < 0.01f)
return default;
// 透视参数(根据行号变化)
float rowFactor = frows > 1f ? (float)row / (frows - 1f) : 0.5f;
float d = CameraDistMin + rowFactor * (CameraDistMax - CameraDistMin);
// 块中心坐标
float sx = localX - 0.5f;
float sy = localY - 0.5f;
// === X 方向:完整的透视投影 ===
float denomLeft = Hlsl.Max(d - 0.5f * sinA, 0.01f);
float denomRight = Hlsl.Max(d + 0.5f * sinA, 0.01f);
float projLeftX = -0.5f * cosA * d / denomLeft;
float projRightX = 0.5f * cosA * d / denomRight;
if (sx < projLeftX - 0.001f || sx > projRightX + 0.001f)
return default;
// 逆透视 x
float denom = cosA * d - sx * sinA;
if (Hlsl.Abs(denom) < 0.001f)
return default;
float x3d = sx * d / denom;
if (x3d < -0.5f - 0.001f || x3d > 0.5f + 0.001f)
return default;
// === Y 方向:线性梯形效果(向右收缩) ===
float zRight = 0.5f * sinA;
float rightEdgeScale = d / (d + zRight);
float xNorm = (x3d + 0.5f);
float yScale = 1f - xNorm * (1f - rightEdgeScale);
float y3d = sy / yScale;
if (y3d < -0.5f - 0.001f || y3d > 0.5f + 0.001f)
return default;
// 纹理坐标
float texU_cell = x3d + 0.5f;
float texV_cell = y3d + 0.5f;
float texU = ((float)col + texU_cell) / fcols;
float texV = ((float)row + texV_cell) / frows;
texU = Hlsl.Clamp(texU, 0f, 1f);
texV = Hlsl.Clamp(texV, 0f, 1f);
// 根据当前阶段选择图片
Float4 texColor = showImageA
? imageA.Sample(new float2(texU, texV))
: imageB.Sample(new float2(texU, texV));
// V6: 透明度始终为 1,不进行淡入淡出
float finalAlpha = texColor.W;
return new float4(texColor.X * finalAlpha, texColor.Y * finalAlpha, texColor.Z * finalAlpha, finalAlpha);
}

浙公网安备 33010602011771号