You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

3D立方体投影到2D平面时的缩放功能异常问题求助

3D立方体投影到2D平面时的缩放功能异常问题求助

嘿,我完全理解你想从基础公式慢慢摸索、不依赖现成库的学习思路——这种从底层试错的方式其实能帮你把投影逻辑啃得更扎实!先给你梳理下当前代码里缩放功能的问题,还有具体的调整方向:

核心问题在哪?

你现在用的缩放公式 scale = CameraDistance / (CameraDistance + z2) 是透视投影的简化版,但符号搞反了,再加上顶点Z值的范围太小,才导致缩放效果几乎看不见:

  • 你的立方体顶点原始Z范围是-1到1,乘以CubeSize200后是-200到200,而CameraDistance是400左右,CameraDistance + z2的变化幅度本来就很小;
  • 更关键的是,透视投影的核心是近大远小,但你的公式里,当顶点Z值为负(靠近相机)时,CameraDistance + z2会变小,scale会变大?不对,等下,你这里的Z坐标是世界空间的Z,相机在Z轴正方向的CameraDistance位置,所以靠近相机的顶点Z值应该更小(比如-200),这时候CameraDistance + z2 = 400-200=200,scale=400/200=2;而最远的顶点Z=200,scale=400/(400+200)=0.666——这部分逻辑是对的,但为什么变化不明显?因为你把scale直接乘以了CubeSize200,导致整体缩放的相对变化被稀释了,而且滚轮调整CameraDistance的步长太小,所以视觉上变化微乎其微。

该怎么调整?

先明确你要的是正交投影还是透视投影:

  • 如果是正交投影(没有近大远小,所有物体大小只和距离有关):直接用scale = 基准值 / CameraDistance,比如scale = 800 / CameraDistance,800是你想要的基准缩放系数;
  • 如果是透视投影(要近大远小):需要把顶点Z转换到相机空间,再用标准透视公式。

针对你当前的代码,最简单的修改方案:

  1. 先修正缩放公式的逻辑,把顶点Z转换为相机空间的Z值(相机在原点,看向-Z方向);
  2. 调整缩放系数的计算方式,让CameraDistance的变化能带来更明显的视觉反馈。

调整后的OnPaint核心代码

protected override void OnPaint(PaintEventArgs e)
{
    e.Graphics.Clear(Color.Black);
    e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    var centerX = this.ClientSize.Width / 2f;
    var centerY = this.ClientSize.Height / 2f;

    for (var i = 0; i < this.Cube.Length; i++)
    {
        var p = this.Cube[i];

        // 绕X轴旋转
        var y1 = (p.Y * (float)Math.Cos(this.CameraAngleX)) - (p.Z * (float)Math.Sin(this.CameraAngleX));
        var z1 = (p.Y * (float)Math.Sin(this.CameraAngleX)) + (p.Z * (float)Math.Cos(this.CameraAngleX));

        // 绕Y轴旋转
        var x2 = (p.X * (float)Math.Cos(this.CameraAngleY)) - (z1 * (float)Math.Sin(this.CameraAngleY));
        var z2 = (p.X * (float)Math.Sin(this.CameraAngleY)) + (z1 * (float)Math.Cos(this.CameraAngleY));

        // 转换到相机空间:相机在(0,0,CameraDistance),所以相机空间Z值 = z2 - CameraDistance(此时相机看向-Z方向,前方的点Z为负)
        var z_camera = z2 - this.CameraDistance;
        // 透视缩放:focalLength设为和CubeSize匹配的值,让初始缩放比例合适
        float focalLength = this.CubeSize * 2;
        var scale = focalLength / (-z_camera); // z_camera是负数,取反后为正,保证scale为正

        // 计算2D投影位置
        this.CubeProjected[i] = new PointF(
            (x2 * scale * this.CubeSize) + centerX,
            (y1 * scale * this.CubeSize) + centerY
        );
    }

    // 绘制立方体的边
    using (var pen = new Pen(Color.LightBlue, 2))
    {
        for (int i = 0; i < this.EdgesDimensionLength0; i++)
        {
            var p1 = this.CubeProjected[this.Edges[i, 0]];
            var p2 = this.CubeProjected[this.Edges[i, 1]];
            e.Graphics.DrawLine(pen, p1, p2);
        }
    }
}

调整后CameraDistance的预期效果

  • 滚轮向前滚动(减小CameraDistance):立方体快速变大,靠近相机的面会比远的面大得多,透视效果明显;
  • 滚轮向后滚动(增大CameraDistance):立方体快速缩小,透视效果逐渐减弱,最终接近正交投影的效果;
  • 你可以调整focalLength的数值,比如改成CubeSize*1.5或CubeSize*3,来控制初始的缩放比例和透视强度。

完整可编译代码(已修正缩放逻辑)

using System;
using System.Drawing;
using System.Windows.Forms;

namespace GeometricVisualizer
{
    internal static class Program
    {
        [STAThread]
        private static void Main() => Application.Run(new FormCube());
    }

    public struct Point3D
    {
        public float X, Y, Z;
        public Point3D(float x, float y, float z)
        {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }
    }

    public partial class FormCube : Form
    {
        private Point MousePositionLast;
        private bool MouseStateIsDragging;
        private float CameraAngleX = 0, CameraAngleY = 0, CubeSize = 200, CameraDistance = 400;
        private int EdgesDimensionLength0 = 12;
        private int[,] Edges = new int[,]
        {
            {0, 1}, {1, 2}, {2, 3}, {3, 0},
            {4, 5}, {5, 6}, {6, 7}, {7, 4},
            {0, 4}, {1, 5}, {2, 6}, {3, 7},
        };
        private Point3D[] Cube = new Point3D[]
        {
            new Point3D(-1, -1, -1), new Point3D(1, -1, -1),
            new Point3D(1, 1, -1), new Point3D(-1, 1, -1),
            new Point3D(-1, -1, 1), new Point3D(1, -1, 1),
            new Point3D(1, 1, 1), new Point3D(-1, 1, 1),
        };
        private PointF[] CubeProjected = new PointF[8];

        public FormCube()
        {
            this.EdgesDimensionLength0 = this.Edges.GetLength(0);
            this.KeyPreview = true;
            this.DoubleBuffered = true;
            this.Text = "3D Cube with Mouse Rotation & Zoom";
            this.WindowState = FormWindowState.Maximized;
            this.KeyDown += this.FormCube_KeyDown;
            this.MouseUp += this.FormCube_MouseUp;
            this.MouseDown += this.FormCube_MouseDown;
            this.MouseMove += this.FormCube_MouseMove;
            this.MouseWheel += this.FormCube_MouseWheel;
            this.Resize += this.FormCube_Resize;
            this.ResizeEnd += this.FormCube_Resize;
            this.ResizeBegin += this.FormCube_Resize;
        }

        private void FormCube_Resize(object? sender, EventArgs e) => this.Invalidate();

        private void FormCube_KeyDown(object? sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Up: this.CameraAngleX -= 0.1f; break;
                case Keys.Down: this.CameraAngleX += 0.1f; break;
                case Keys.Left: this.CameraAngleY -= 0.1f; break;
                case Keys.Right: this.CameraAngleY += 0.1f; break;
                case Keys.Add: case Keys.Oemplus:
                    this.CameraDistance = Math.Max(100, this.CameraDistance - 20); break;
                case Keys.Subtract: case Keys.OemMinus:
                    this.CameraDistance = Math.Min(1000, this.CameraDistance + 20); break;
                case Keys.R:
                    this.CameraAngleX = 0; this.CameraAngleY = 0; this.CameraDistance = 400; break;
            }
            this.Invalidate();
        }

        private void FormCube_MouseDown(object? sender, MouseEventArgs e)
        {
            this.MouseStateIsDragging = true;
            this.MousePositionLast = e.Location;
        }

        private void FormCube_MouseMove(object? sender, MouseEventArgs e)
        {
            if (this.MouseStateIsDragging)
            {
                var dx = e.X - this.MousePositionLast.X;
                var dy = e.Y - this.MousePositionLast.Y;
                this.CameraAngleY += dx * 0.01f;
                this.CameraAngleX += dy * 0.01f;
                this.MousePositionLast = e.Location;
                this.Invalidate();
            }
        }

        private void FormCube_MouseUp(object? sender, MouseEventArgs e) => this.MouseStateIsDragging = false;

        private void FormCube_MouseWheel(object? sender, MouseEventArgs e)
        {
            this.CameraDistance -= e.Delta * 0.1f;
            this.CameraDistance = Math.Max(100, Math.Min(1000, this.CameraDistance));
            this.Invalidate();
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            e.Graphics.Clear(Color.Black);
            e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            var centerX = this.ClientSize.Width / 2f;
            var centerY = this.ClientSize.Height / 2f;

            for (var i = 0; i < this.Cube.Length; i++)
            {
                var p = this.Cube[i];

                // 绕X轴旋转
                var y1 = (p.Y * (float)Math.Cos(this.CameraAngleX)) - (p.Z * (float)Math.Sin(this.CameraAngleX));
                var z1 = (p.Y * (float)Math.Sin(this.CameraAngleX)) + (p.Z * (float)Math.Cos(this.CameraAngleX));

                // 绕Y轴旋转
                var x2 = (p.X * (float)Math.Cos(this.CameraAngleY)) - (z1 * (float)Math.Sin(this.CameraAngleY));
                var z2 = (p.X * (float)Math.Sin(this.CameraAngleY)) + (z1 * (float)Math.Cos(this.CameraAngleY));

                // 转换到相机空间并计算透视缩放
                var z_camera = z2 - this.CameraDistance;
                float focalLength = this.CubeSize * 2;
                var scale = focalLength / (-z_camera);

                // 计算2D投影位置
                this.CubeProjected[i] = new PointF(
                    (x2 * scale * this.CubeSize) + centerX,
                    (y1 * scale * this.CubeSize) + centerY
                );
            }

            // 绘制立方体的边
            using (var pen = new Pen(Color.LightBlue, 2))
            {
                for (int i = 0; i < this.EdgesDimensionLength0; i++)
                {
                    var p1 = this.CubeProjected[this.Edges[i, 0]];
                    var p2 = this.CubeProjected[this.Edges[i, 1]];
                    e.Graphics.DrawLine(pen, p1, p2);
                }
            }
        }
    }
}

额外小提示

  • 你可以把focalLength改成可调整的参数,比如在键盘事件里加个快捷键来增减它,这样能更直观地感受焦距对透视效果的影响;
  • 等你把这个基础逻辑玩熟了,再去学矩阵变换会轻松很多——本质上矩阵就是把旋转、平移、投影这些步骤打包成数学运算,代码会更简洁,但底层逻辑和你现在试的是一样的。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:12:59