Warning: file_get_contents(https://whois.pconline.com.cn/jsLabel.jsp?ip=127.0.0.1) [function.file-get-contents]: failed to open stream: HTTP request failed! HTTP/1.1 503 Service Temporarily Unavailable in D:\wwwroot\huidong\wwwroot\function.inc.php on line 884
2D,3D坐标旋转问题,3D模型如何投影到2D平面【无需矩阵,很简单】 - huidong

huidong

首页 | 会员登录 | 关于争取 2022 寒假做出汇东网 Ver3.0.0 !
搜索文章



2022.4.30 更新:请看更新的博客 http://huidong.xyz/index.php?mode=2&id=406 


首先!没图没真相,没图没人看,放个效果图先:

你将在学习3D旋转后实现如下效果:

录制_2021_01_23_20_18_31_600.gif


你将在学习2D旋转后实现如下效果:


(2021.8.2 新增一张图片:)  





录制_2021_01_23_19_23_14_595.gif


上图记录了一个点绕着圆心旋转画圆的过程。

2D旋转


////////////////////////  2021.7.21新增 /////////////////////////////


2D的旋转很好理解,不需要任何复杂的公式,只需要三角函数就可以搞定。

假设平面内有一点 A,顺时针或者逆时针旋转 α° 到 A',怎么求 A' 的坐标?这里需要明确的是:A点是绕着原点O在旋转,在这种情况下,AO的长度一定等于A'O,既然如此,如下图,A'M和AN都垂直于x轴,我们可以根据A点的坐标(已知)算出∠AON的tan,然后再得出∠AON的度数。

已知∠α = ∠AOA' ,α是旋转度数,是已知的,那么同时∠AON也是知道的,那么 ∠A'ON = ∠AOA' + ∠AON,得到∠A'ON了,我们又知道 OA' = OA,三角函数一算不就知道 A'M 和 OM 的值了吗? A' 的坐标也就出来了。

scrawl.png


至于3D的旋转,其实可以转换成三次二维的旋转。每一次的三维旋转都可以拆分成让一个三维点绕 x,y,z轴各旋转多少度,绕某个轴旋转其实就是将这个三维点所在的yOz平面,或者xOz平面,或者xOy平面进行旋转。只需要取这个平面对应的坐标,比如xOy要旋转,就取这个三维点的xy坐标,然后用二维旋转的方法旋转指定度数,其它两个平面进行一样的操作即可。


// 2021.8.2 新增 //




此图效果源码:

注:使用了图形库 easyx 20210730

#include <easyx.h>
#include <math.h>

inline double ConvertToRadian(double angle)
{
    return 3.1415926535 / 180.0 * angle;
}

inline POINT Rotate2D(int x, int y, double angle)
{
    double dRotationRadian = ConvertToRadian(angle);
    double dPositionRadian = x == 0 ? 0 : atan(y / x);
    double dHypotenuse = sqrt(x * x + y * y);
    POINT p;
    p.x = (LONG)(cos(dRotationRadian + dPositionRadian) * dHypotenuse);
    p.y = (LONG)(sin(dRotationRadian + dPositionRadian) * dHypotenuse);
    return p;
}

inline POINT Rotate2D(POINT p, double angle)
{
    return Rotate2D(p.x, p.y, angle);
}

int main()
{
    initgraph(640, 480);
    setorigin(320, 240);

    POINT p{ 0,0 };
    double angle = 0;

    while (++angle < 7200)
    {
        POINT pNew = Rotate2D(p, angle);
        putpixel(pNew.x, pNew.y, WHITE);

        // 使这个圆逐渐扩大
        if ((int)angle % 20 == 0)
        {
            p.x++;
            p.y++;
        }
    }

    getmessage(EM_CHAR);

    closegraph();
    return 0;
}


可以总结 2D 旋转的公式:
旋转后的点 A' 的坐标:
x2 = cos( 旋转角弧度 + atan( y / x ) ) * sqrt( x^2 + y^2 )
y2 = sin( 旋转角弧度 + atan( y / x ) ) * sqrt( x^2 + y^2 )


这个公式在上面的代码中使用了,至于效果图中的圆在逐渐扩大是刻意制造的效果。

下面的内容中使用的公式是我之前写的,有一点小小的不同。


/////////////////////// 新增内容结束 //////////////////////////


现在来讨论2D旋转,设有如下坐标系:

1611149203929916.png

已知点A(1,2),α为OA和Ox的夹角,AE垂直于Ox,三角形OAE是直角三角形,那么,勾股定理,OA = 根号下1的平方+2的平方 = 根号5


已知三角形OAE的三个边,现在可以求α

已知sin = 对边 / 斜边,α = arcsin(sin α) = arcsin(对边 / 斜边) = arcsin(1/根号5) ≈ 26°


现在知道了α,现将点A进行旋转。点A是(1,2),这其实是相对于坐标系xOy而言的,如果我们把坐标系旋转,然后得到相对于新的坐标系的点A的位置,不就相当于得到了点A旋转后的坐标了吗?

于是我们将xOy旋转 逆时针旋转β 为x'Oy':

图片.png


在上图中,α = ∠AOx,坐标系的旋转角 = ∠xOx',就是β,这个是已知量。


那么对于新的坐标系x'Oy'而言,和x'的夹角是∠AOx' = α - β,这是第一个已知条件

在新的坐标系中,OA是不变的,那么就是说,三角形的斜边不变,这是第二个已知条件。有了斜边和夹角,求点A到y'的距离(即新的x坐标)和到x'的距离(即新的y坐标)不是很轻松吗?

具体求法如下:


已知OA = 根号5,夹角为(α - β),设点A垂直x'于H,垂直y'于F,如图:

图片.png

三角形OAH是直角三角形,不用说的了。


根据sin(角) = 对边 / 斜边,

可以知道:对边 = 斜边 * sin(角)

又因为在三角形OAH中,AH是对边,OA是斜边,OH是邻边,所以可以得到如下等式:

AH = OA * sin(α - β)


又因为AF 平行且相等于 OH,这个很容易看出来。所以,求AF相当于求OH,

这个时候我们知道直角三角形的斜边,对边,可以勾股定理求斜边的了,但是这里还有另外一种方式:

OH又是邻边,我们知道cos(角) = 邻边 / 斜边,所以:邻边 = cos(角) * 斜边,则:

OH = cos(α - β) * OA


这下,OH和AH都有了,A点的新坐标不就是:A'(OH, AH) 了嘛!


需要注意的是,sin和cos用的是弧度而不是度,需要进行转换,转换方式:


弧度 = π / 180 * 度数

度数 = 180 / π * 弧度


总结:

A'( cos( arcsin(对边 / 斜边) - 坐标系旋转弧度 ) * 斜边,sin( arcsin(对边 / 斜边) - 坐标系旋转弧度 ) * 斜边 )


C语言例子:

#include <easyx.h>
#include <math.h>
#include <conio.h>
#include <stdio.h>

// π
#define PI 3.1415926535

// 点信息结构体
struct dPOINT
{
    double x;
    double y;
};

// 2D旋转函数
// p        原坐标
// angle    旋转角度(正数为逆时针,负数为顺时针)
// 返回值为旋转后的坐标
dPOINT rotate2D(dPOINT p, double angle)
{
    // 旋转弧度
    double radian = PI / 180 * angle;

    // 斜边
    double hypotenuse = sqrt(pow(p.x, 2) + pow(p.y, 2));

    return dPOINT{ cos(asin(p.y / hypotenuse) - radian) * hypotenuse,sin(asin(p.y / hypotenuse) - radian) * hypotenuse };
}

int main()
{
    // 图形界面初始化
    initgraph(640, 480, EW_SHOWCONSOLE);
    setbkcolor(BLUE);
    cleardevice();

    // 设置绘图原点
    setorigin(320, 240);

    // 存储点位置
    dPOINT p = { 0 };

    for (int i = 0;; i++)
    {
        // 输出点
        putpixel((int)p.x, (int)p.y, WHITE);

        // 输出旋转次数,点坐标
        printf("%d : %f, %f\n", i, p.x, p.y);

        // 按一次任意键,旋转一次点
        _getch();

        // 将点 (200, 100) 旋转 i 度,得到的点坐标存到 p 中。
        p = rotate2D(dPOINT{ 200, 100 }, i);
    }

    // 关闭窗口
    closegraph();
    return 0;
}

编译环境:VS2019 + EasyX 20200902

这个例子中使用了EasyX图形库,用于绘制旋转后的点,后面的例子中都会用到此图形库。

没有安装此图形库没关系,上述的代码中,核心都是rotate2D这个函数,是没有用到图像库的。


编译上述代码,程序运行时,长按任意键,可以看到点在不停的旋转,画出来了一个虚线的圆,说明旋转已经成功实现了。效果如下:

录制_2021_01_23_19_23_14_595.gif


上面有一段代码调用rotate2D函数,是这样的:

// 将点 (200, 100) 旋转 i 度,得到的点坐标存到 p 中。
p = rotate2D(200, 100, i);

这里用的办法是将一个固定的点旋转一个一直累加的度数,那么可不可以每次都旋转固定的度数,但是每次都把新的点更新到p中呢?

事实上不可以。因为每次旋转一个指定的度数,有可能double精度不够,这个点旋转这么多度正好没变化,那不就酿成死循环,得不到新的坐标了嘛。还有一点更为重要:double精度不够,丢失精度后,旋转点得到的坐标和实际坐标偏差会累加,最终导致坐标严重偏移,这一现象在接下来的3D旋转中更是明显地体现出来。


至于2D平移……就是坐标的加减,无需赘述了。


3D旋转和投影到2D平面问题

刚才我们已经解决了2D旋转问题。在这个前提下,接下来的内容将会非常简单。

网上的许多涉及2D,3D旋转的教学都用了矩阵进行运算,或者说欧拉角,四元数啥的。我并非认为他们不好,但是就凭我一个初中生的能力自学它们貌似确实有点困难,所以,接下来的内容完全不涉及它们,因此也变得十分简单。


我们来看3D旋转是怎么旋转的,有三种方式:

  1. 绕x轴转

  2. 绕y轴转

  3. 绕z轴转



其实也不难发现,就是绕着3维坐标系的三个轴转,而且绕着某个轴转时,比如绕x轴转,是不是x坐标不变,y,z坐标发生改变?你可以自己模拟一下,看看是否如此。其它两个轴也是这样,绕一个轴转,这个轴的坐标不变,其它两个轴的坐标发生改变。


那么就说明,一个三维旋转可以拆分成绕三个轴的旋转,而绕其中一个轴旋转,只有另外两个坐标发生改变,那就可以直接套入2D旋转公式进行计算。


首先有一个存储3D坐标的结构体:

// 3D 点信息结构体
struct dPOINT3D
{
    double x;
    double y;
    double z;
};


举个例子,比如我有点 A(1, 2, 3)

dPOINT3D pA = { 1,2,3 };


绕x轴转60°,绕z轴转20°,不绕y轴转。

那么可以首先绕x轴转,绕x轴,是不是就是x坐标不变,y坐标和z坐标变?那就把z坐标和y坐标带入2D旋转代码:

dPOINT pTemp;    // 临时存储2D旋转的结果

// 绕x轴转(rotate2D 函数见上面的2D旋转实现代码)
pTemp = rotate2D(dPOINT{ pA.y,pA.z }, 60);

// 这时,pTemp的x和y存储的就分别是pA绕x轴旋转后的y坐标和z坐标了
// 于是将新的坐标值放回pA中
pA.y = pTemp.x;
pA.z = pTemp.y;


然后是绕z轴转,同理,x坐标和y坐标变化,所以:

// 绕z轴转
pTemp = rotate2D(dPOINT{ pA.x,pA.y }, 20);

// 这时,pTemp的x和y存储的就分别是pA绕z轴旋转后的x坐标和y坐标了
pA.x = pTemp.x;
pA.y = pTemp.y;


这两次2D旋转下来,就间接地完成了一次3D旋转,如果这次3D旋转还需要绕y轴转的话,就还要2D旋转一次。


总结:一次3D旋转可以分解为3次2D平面的旋转,分别是xOy,xOz和yOz平面的旋转,也就是绕xyz三轴的旋转。所以一次3D旋转需要给出3个方向的旋转角度,某个方向不旋转,则角度可以为0。绕其中一个轴旋转,只有另外两个坐标发生改变,就表名可以直接套入2D旋转公式进行计算,计算出的新坐标可直接放回3D点中。



现在旋转是解决了,但是怎么投影到2D平面?其实也很简单。

现在我们有三维坐标系xyz,如下图

图片.png

我们该如何观察这个立方体?

我们需要站在一个平面去观察它,对吧。这个平面其实很好选择,我们可以直接在三维坐标系中找一个平面来当作目标投影平面。比如就直接选平面xOy也可以(原点O忘记标了),然后呢?那我们直接取所有3D点的x坐标和y坐标出来,因为以xOy为投影面嘛,就取xy坐标,然后组成一个2D坐标输出到屏幕上就行了。

对对对,真就这么简单。那你说z坐标不管它了?可以管,也可以不管,因为我们以xOy作为投影面,那么z坐标表示的就是点到xOy平面的距离。距离有什么用?我们都知道我们看东西是近大远小的,那根据点和我们的距离就可以知道这个点应该缩小多少嘛。如果你暂时不需要近大远小的视觉效果,你现在就可以先不管z坐标。同理,如果我们以yOz为观察面,就可以取所有3D点的y和z坐标,不管x坐标;以xOz为观察面也同理。


那么现在我们来造一个3D模型试试效果吧?

我们来构建一个立体三角形,它看起来像这样:

图片.png

它的端点有:

一个顶点(40, 120, 20)

四个底部的端点(20, 0, 40),(60, 0, 40),(60, 0, 0),(20, 0, 0)


我们还需要写三个新的函数来分别进行X, Y, Z轴的旋转,整个示例代码看起来像这样:

#include <easyx.h>
#include <math.h>
#include <conio.h>
#include <stdio.h>

// π
#define PI 3.1415926535

// 点信息结构体
struct dPOINT
{
    double x;
    double y;
};

// 3D 点信息结构体
struct dPOINT3D
{
    double x;
    double y;
    double z;
};

// 2D旋转函数
// p        原坐标
// angle    旋转角度(正数为逆时针,负数为顺时针)
// 返回值为旋转后的坐标
dPOINT rotate2D(dPOINT p, double angle)
{
    // 旋转弧度
    double radian = PI / 180 * angle;

    // 斜边
    double hypotenuse = sqrt(pow(p.x, 2) + pow(p.y, 2));

    return dPOINT{ cos(asin(p.y / hypotenuse) - radian) * hypotenuse,sin(asin(p.y / hypotenuse) - radian) * hypotenuse };
}

// 3D 点绕 x 轴旋转
// p_p3D    原坐标
// angle    旋转度数
dPOINT3D rotate3D_X(dPOINT3D p_p3D, double angle)
{
    dPOINT3D p3D = p_p3D;
    dPOINT pTemp = rotate2D(dPOINT{ p3D.y,p3D.z }, angle);
    p3D.y = pTemp.x;
    p3D.z = pTemp.y;
    return p3D;
}

// 3D 点绕 y 轴旋转
// p_p3D    原坐标
// angle    旋转度数
dPOINT3D rotate3D_Y(dPOINT3D p_p3D, double angle)
{
    dPOINT3D p3D = p_p3D;
    dPOINT pTemp = rotate2D(dPOINT{ p3D.x,p3D.z }, angle);
    p3D.x = pTemp.x;
    p3D.z = pTemp.y;
    return p3D;
}

// 3D 点绕 z 轴旋转
// p_p3D    原坐标
// angle    旋转度数
dPOINT3D rotate3D_Z(dPOINT3D p_p3D, double angle)
{
    dPOINT3D p3D = p_p3D;
    dPOINT pTemp = rotate2D(dPOINT{ p3D.x,p3D.y }, angle);
    p3D.x = pTemp.x;
    p3D.y = pTemp.y;
    return p3D;
}

int main()
{
    // 图形界面初始化
    initgraph(640, 480, EW_SHOWCONSOLE);
    setbkcolor(BLUE);
    cleardevice();

    // 设置绘图原点
    setorigin(320, 240);

    // 存储 3D 点位置(立体三角形ABCDE)
    dPOINT3D pA = { 20,0,40 };
    dPOINT3D pB = { 60,0,40 };
    dPOINT3D pC = { 60,0,0 };
    dPOINT3D pD = { 20,0,0 };
    dPOINT3D pE = { 40,120,20 };

    // 存储旋转后的点
    dPOINT3D pA2 = { 0 }, pB2 = { 0 }, pC2 = { 0 }, pD2 = { 0 }, pE2 = { 0 };

    
    // 对所有点都先进行XY轴的旋转,这样,在待会的旋转中,三角形就显得更有立体感
    pA = rotate3D_Y(pA, 60);
    pB = rotate3D_Y(pB, 60);
    pC = rotate3D_Y(pC, 60);
    pD = rotate3D_Y(pD, 60);
    pE = rotate3D_Y(pE, 60);

    pA = rotate3D_X(pA, 60);
    pB = rotate3D_X(pB, 60);
    pC = rotate3D_X(pC, 60);
    pD = rotate3D_X(pD, 60);
    pE = rotate3D_X(pE, 60);

    for (int i = 1;; i++)
    {
        // 连线(以xOy为观察面,只取xy坐标)
        line((int)pA2.x, (int)pA2.y, (int)pB2.x, (int)pB2.y);
        line((int)pB2.x, (int)pB2.y, (int)pC2.x, (int)pC2.y);
        line((int)pC2.x, (int)pC2.y, (int)pD2.x, (int)pD2.y);
        line((int)pD2.x, (int)pD2.y, (int)pA2.x, (int)pA2.y);
        line((int)pA2.x, (int)pA2.y, (int)pE2.x, (int)pE2.y);
        line((int)pB2.x, (int)pB2.y, (int)pE2.x, (int)pE2.y);
        line((int)pC2.x, (int)pC2.y, (int)pE2.x, (int)pE2.y);
        line((int)pD2.x, (int)pD2.y, (int)pE2.x, (int)pE2.y);

        // 输出旋转次数,点坐标
        printf(
            "%d :\tA( %f, %f, %f )\n"
            "\tB( %f, %f, %f )\n"
            "\tC( %f, %f, %f )\n"
            "\tD( %f, %f, %f )\n"
            "\tE( %f, %f, %f )\n"
            , i, pA2.x, pA2.y, pA2.z,
            pB2.x, pB2.y, pB2.z,
            pC2.x, pC2.y, pC2.z,
            pD2.x, pD2.y, pD2.z,
            pE2.x, pE2.y, pE2.z
        );

        // 动画延时
        Sleep(10);

        // 清屏
        cleardevice();

        // 所有点绕Z轴旋转
        pA2 = rotate3D_Z(pA, i);
        pB2 = rotate3D_Z(pB, i);
        pC2 = rotate3D_Z(pC, i);
        pD2 = rotate3D_Z(pD, i);
        pE2 = rotate3D_Z(pE, i);
    }

    // 关闭窗口
    closegraph();
    return 0;
}


旋转效果:

录制_2021_01_23_20_18_31_600.gif


看起来还不错!

你当然可以自己更改上述代码中的观察视角、旋转轴、旋转度数,看看会有什么效果。






返回首页


Copyright (C) 2018-2024 huidong