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转换到相机空间,再用标准透视公式。
针对你当前的代码,最简单的修改方案:
- 先修正缩放公式的逻辑,把顶点Z转换为相机空间的Z值(相机在原点,看向-Z方向);
- 调整缩放系数的计算方式,让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
相关产品推荐
相关产品推荐

