用相机观察线条
2023-5-7 ~ 2023-6-16
(0)
程序截图
操作说明
鼠标拖动转换摄像机朝向,鼠标滚轮使摄像机往朝向方向前进或后退,中间那个圆圈是摄像机屏幕大小,再远的地方摄像机能看到的只有这么大的区域,线条不会超出这个圆圈的区域。这里观察的两条线条为互相垂直的两条线条。
编写步骤
① 坐标系转换,以相机绝对位置为原点 O 计算其他顶点的相对位置,然后以相机 X 轴,Y 轴方向的单位向量点乘顶点的相对位置,得到顶点在相机坐标系中的坐标。推荐相机坐标系用左手坐标系。
② 三维空间中设一条线 AB 的两端点
列出以下方程:
令
也就是一个简单的一元二次方程,求解得到 k 的值后完成第二步。
③ 对所求出来的结果进行分析,为了简化计算,这里进行了开平方的操作,所以实际上 k 的值包含了直线与 Z 轴方向夹角为 π - θ 的情况,过滤掉所有错误的情况后得到在屏幕内的唯二的两个点的三维坐标。此坐标 Z 轴的值应该为正值,这样比较好计算,然后令三维点变成原点到三维点的向量进行缩放操作得到一个 Z 坐标值为透视距离的三维向量,此向量的 X,Y 值即为该点透视投影后在屏幕上的坐标值。
代码实现
////////////////////////////////////////////
// 程序:用相机观察线条
// 作者:我想做三国志
// 编译环境:Visual Studio 2022,EasyX_20220901
// 编写日期:2023-5-7
#include <graphics.h>
#include <math.h>
#include <vector>
const double PI = 3.1415926536; // π
const int WIDTH = 640; // 屏幕宽度
const int HEIGHT = 480; // 屏幕高度
const int GAMEPAD = 60; // 游戏手柄,移动多少距离旋转 60
using std::vector;
// 三维向量
class Vec3
{
public:
double xx, yy, zz;
// 构造函数
Vec3(double xx = 0, double yy = 0, double zz = 0) : xx(xx), yy(yy), zz(zz) {}
// 向量相加
Vec3 operator+(Vec3 num) { return Vec3(this->xx + num.xx, this->yy + num.yy, this->zz + num.zz); }
// 向量乘法
Vec3 operator*(double num) { return Vec3(this->xx * num, this->yy * num, this->zz * num); }
// 向量点乘
double operator*(Vec3 num) { return this->xx * num.xx + this->yy * num.yy + this->zz * num.zz; }
// 向量除法
Vec3 operator/(double num) { return Vec3(this->xx / num, this->yy / num, this->zz / num); }
// 向量相减
Vec3 operator-(Vec3 num) { return Vec3(this->xx - num.xx, this->yy - num.yy, this->zz - num.zz); }
// 得到此向量模长
double GetLength() { return sqrt(this->xx * this->xx + this->yy * this->yy + this->zz * this->zz); }
// 得到两向量之间的 cos 值
double GetCosBetween(Vec3 num) { return (*this) * num / this->GetLength() / num.GetLength(); }
// 得到此向量的单位向量
Vec3 GetUnitVector() { return (*this) / this->GetLength(); }
// 得到此向量在另一个向量上的投影
Vec3 GetProjectionTo(Vec3 num) { return num.GetUnitVector() * (this->GetCosBetween(num) * this->GetLength()); }
// 向量叉乘
Vec3 MultiplicationCross(Vec3 num) { return Vec3(this->yy * num.zz - this->zz * num.yy, -this->xx * num.zz + this->zz * num.xx, this->xx * num.yy - this->yy * num.xx); }
// 求将此向量关于 X 轴,Y 轴,Z 轴旋转 a、b、c 度后的向量
Vec3 GetRotateVec(double a, double b, double c)
{
Vec3 result = this->GetUnitVector();
result = Vec3(result.xx, result.yy * cos(a) - result.zz * sin(a), result.zz * cos(a) + result.yy * sin(a)).GetUnitVector();
result = Vec3(result.xx * cos(b) - result.zz * sin(b), result.yy, result.zz * cos(b) + result.xx * sin(b)).GetUnitVector();
result = Vec3(result.xx * cos(c) - result.yy * sin(c), result.yy * cos(c) + result.xx * sin(c), result.zz).GetUnitVector();
return (result * this->GetLength());
}
};
// 二维向量
class Vec2
{
public:
double xx, yy;
// 构造函数
Vec2(double xx = 0, double yy = 0) : xx(xx), yy(yy) {}
// 向量相加
Vec2 operator+(Vec2 num) { return Vec2(this->xx + num.xx, this->yy + num.yy); }
// 向量乘法
Vec2 operator*(double num) { return Vec2(this->xx * num, this->yy * num); }
// 向量点乘
double operator*(Vec2 num) { return this->xx * num.xx + this->yy * num.yy; }
// 向量除法
Vec2 operator/(double num) { return Vec2(this->xx / num, this->yy / num); }
// 向量相减
Vec2 operator-(Vec2 num) { return Vec2(this->xx - num.xx, this->yy - num.yy); }
// 得到此向量模长
double GetLength() { return sqrt(this->xx * this->xx + this->yy * this->yy); }
// 得到两向量之间的 cos 值
double GetCosBetween(Vec2 num) { return (*this) * num / this->GetLength() / num.GetLength(); }
// 得到此向量的单位向量
Vec2 GetUnitVector() { return (*this) / this->GetLength(); }
// 得到此向量在另一个向量上的投影
Vec2 GetProjectionTo(Vec2 num) { return num.GetUnitVector() * (this->GetCosBetween(num) * this->GetLength()); }
// 得到此向量旋转 angle 后的向量
Vec2 GetRotateVec(double angle) { return Vec2(this->xx * cos(angle) - this->yy * sin(angle), this->yy * cos(angle) + this->xx * sin(angle)); }
};
// Camera 得用左手坐标系,视角为 120°
class Camera
{
public:
Vec3 XAcross, YAcross, position;
double Angle;
double focal_length;
Camera()
{
XAcross = Vec3(1, 0, 0);
YAcross = Vec3(0, 1, 0);
position = Vec3(0, 0, 0);
Angle = PI / 3;
focal_length = 100;
// 320/sqrt(3) 240/sqrt(3)
}
void HorizontalRotate(double angle)
{
Vec3 Z_Across = YAcross.MultiplicationCross(XAcross).GetUnitVector();
XAcross = XAcross * cos(angle) + Z_Across * sin(angle);
XAcross = XAcross.GetUnitVector();
}
void VerticalRotate(double angle)
{
Vec3 Z_Across = YAcross.MultiplicationCross(XAcross).GetUnitVector();
YAcross = YAcross * cos(angle) + Z_Across * sin(angle);
YAcross = YAcross.GetUnitVector();
}
void GoAhead(double times)
{
Vec3 Z_Across = YAcross.MultiplicationCross(XAcross).GetUnitVector();
position = position + Z_Across * times;
}
};
void DrawLine3D(Camera camera, Vec3 pos1, Vec3 pos2, Vec2 pericenter)
{
bool flag0 = false, flag1 = false;
double angle = camera.Angle;
Vec3 ZAcross = camera.YAcross.MultiplicationCross(camera.XAcross).GetUnitVector();
Vec3 XAcross = camera.XAcross.GetUnitVector();
Vec3 YAcross = camera.YAcross.GetUnitVector();
Vec3 p1 = pos1 - camera.position;
Vec3 p2 = pos2 - camera.position;
Vec3 transform_p1 = Vec3(p1 * XAcross, p1 * YAcross, p1 * ZAcross); // 转化坐标系
Vec3 transform_p2 = Vec3(p2 * XAcross, p2 * YAcross, p2 * ZAcross); // 转化坐标系
if (transform_p1.GetLength() < DBL_MIN || transform_p1.zz / transform_p1.GetLength() >= cos(angle))flag0 = true; // transform_p1 在屏幕内
if (transform_p2.GetLength() < DBL_MIN || transform_p2.zz / transform_p2.GetLength() >= cos(angle))flag1 = true; // transform_p2 在屏幕内
if (flag0 && flag1)
{
Vec3 lp0 = transform_p1 * (camera.focal_length / transform_p1.zz);
Vec3 lp1 = transform_p2 * (camera.focal_length / transform_p2.zz);
line(lp0.xx + pericenter.xx, lp0.yy + pericenter.yy, lp1.xx + pericenter.xx, lp1.yy + pericenter.yy);
setfillcolor(RED);
solidcircle(lp0.xx + pericenter.xx, lp0.yy + pericenter.yy, 3);
setfillcolor(GREEN);
solidcircle(lp1.xx + pericenter.xx, lp1.yy + pericenter.yy, 3);
return;
}
// 这里事先过滤一下,避免计算复杂
// 穿过圆心的应该过滤
double del_z = transform_p2.zz - transform_p1.zz;
double del_y = transform_p2.yy - transform_p1.yy;
double del_x = transform_p2.xx - transform_p1.xx;
double Cos = cos(angle) * cos(angle);
double a = ((1 - Cos) * del_z * del_z - Cos * del_y * del_y - Cos * del_x * del_x);
double b = 2 * ((1 - Cos) * del_z * transform_p1.zz - Cos * del_x * transform_p1.xx - Cos * del_y * transform_p1.yy);
double c = ((1 - Cos) * transform_p1.zz * transform_p1.zz - Cos * transform_p1.xx * transform_p1.xx - Cos * transform_p1.yy * transform_p1.yy);
double delta = b * b - 4 * a * c;
if (delta < 0.0)return;
else if (delta > DBL_MIN)
{
double k0 = (-b + sqrt(delta)) / 2 / a;
double k1 = (-b - sqrt(delta)) / 2 / a;
bool flag2 = false, flag3 = false;
if (transform_p1.zz + del_z * k0 < 0)flag2 = true; // 点三在相机背后
if (transform_p1.zz + del_z * k1 < 0)flag3 = true; // 点四在相机背后
if (flag2 && flag3)return;
if (!flag2 && !flag3 && (k0 < 0 && k1 < 0 || k0>1 && k1>1))return;
// 至少有一个点在屏幕内
double k2 = 0;
double k3 = 0;
if (!flag2 && !flag3 && (k0 >= 0 && k0 <= 1 && k1 >= 0 && k1 <= 1))
{
k2 = min(k0, k1);
k3 = max(k0, k1);
}
else if (flag0)
{
k2 = 0;
if (!flag2 && k0 >= 0 && k0 <= 1)k3 = k0;
else if (!flag3 && k1 >= 0 && k1 <= 1)k3 = k1;
}
else if (flag1)
{
if (!flag2 && k0 >= 0 && k0 <= 1)k2 = k0;
else if (!flag3 && k1 >= 0 && k1 <= 1)k2 = k1;
k3 = 1;
}
else return;
Vec3 lp0 = transform_p1 + (transform_p2 - transform_p1) * k2;
Vec3 lp1 = transform_p1 + (transform_p2 - transform_p1) * k3;
lp0 = lp0 * (camera.focal_length / lp0.zz);
lp1 = lp1 * (camera.focal_length / lp1.zz);
line(lp0.xx + pericenter.xx, lp0.yy + pericenter.yy, lp1.xx + pericenter.xx, lp1.yy + pericenter.yy);
setfillcolor(RED);
solidcircle(lp0.xx + pericenter.xx, lp0.yy + pericenter.yy, 3);
setfillcolor(GREEN);
solidcircle(lp1.xx + pericenter.xx, lp1.yy + pericenter.yy, 3);
}
else
{
double k = -b / 2 / a;
bool flag = false;
double k2 = 0, k3 = 0;
if (transform_p1.zz + del_z * k < 0)flag = true; // 此点在相机背后
if (flag || !flag0 && !flag1)return;
if (flag0)
{
k2 = 0;
k3 = k;
}
else if (flag1)
{
k2 = k;
k3 = 1;
}
else return;
Vec3 lp0 = transform_p1 + (transform_p2 - transform_p1) * k2;
Vec3 lp1 = transform_p1 + (transform_p2 - transform_p1) * k3;
lp0 = lp0 * (camera.focal_length / lp0.zz);
lp1 = lp1 * (camera.focal_length / lp1.zz);
line(lp0.xx + pericenter.xx, lp0.yy + pericenter.yy, lp1.xx + pericenter.xx, lp1.yy + pericenter.yy);
setfillcolor(RED);
solidcircle(lp0.xx + pericenter.xx, lp0.yy + pericenter.yy, 3);
setfillcolor(GREEN);
solidcircle(lp1.xx + pericenter.xx, lp1.yy + pericenter.yy, 3);
}
}
// 主函数
int main()
{
initgraph(WIDTH, HEIGHT);
BeginBatchDraw();
Vec3 pos1(100, 100, 100), pos2(100, -100, 100), pos3(-100, -100, 100), pos4(-100, 100, 100);
Vec3 pos5(100, 100, -100), pos6(100, -100, -100), pos7(-100, -100, -100), pos8(-100, 100, -100);
Camera camera;
camera.position = Vec3(0, 0, 200);
bool isExit = false;
bool isLPress = false;
Vec2 ori_L;
ExMessage msg;
while (!isExit)
{
while (peekmessage(&msg, EM_KEY | EM_MOUSE))
{
if (msg.message == WM_KEYDOWN)
{
if (msg.vkcode == VK_ESCAPE) isExit = true;
}
else if (msg.message == WM_MOUSEMOVE || msg.message == WM_LBUTTONDOWN || msg.message == WM_LBUTTONUP)
{
if (!isLPress && msg.lbutton)
{
ori_L = Vec2(msg.x, msg.y);
isLPress = true;
}
else if (isLPress && msg.lbutton)
{
Vec2 next = Vec2(msg.x, msg.y);
ori_L = next - ori_L;
double Th = 0;
double Fi = 0;
Th = ori_L.xx / GAMEPAD * PI / 3;
Fi = ori_L.yy / GAMEPAD * PI / 3;
camera.HorizontalRotate(Th);
camera.VerticalRotate(Fi);
ori_L = next;
}
else if (isLPress && !msg.lbutton)
{
isLPress = false;
}
}
else if (msg.message == WM_MOUSEWHEEL)
{
camera.GoAhead(msg.wheel / 6);
}
}
cleardevice();
circle(WIDTH / 2, HEIGHT / 2, 100 * sqrt(3));
DrawLine3D(camera, pos1, pos2, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos2, pos3, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos3, pos4, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos4, pos1, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos1, pos5, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos2, pos6, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos3, pos7, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos4, pos8, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos5, pos6, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos6, pos7, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos7, pos8, Vec2(WIDTH / 2, HEIGHT / 2));
DrawLine3D(camera, pos8, pos5, Vec2(WIDTH / 2, HEIGHT / 2));
FlushBatchDraw();
}
return 0;
}
添加评论
取消回复