C# 实现多种形式的3D翻转页面效果

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

1

需要把一张图片分成 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>

但是使用着色器实现这个效果,却需要从头起步,这可把我难坏了,
1

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

终于调整好了:

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

1

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

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

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

以上的一系列的效果都非常棒。部分算法的核心代码(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);
}
posted @ 2026-03-02 21:00  行人--  阅读(4)  评论(0)    收藏  举报