一个正方体
程序截图
操作方法
鼠标拖动。左键拖动及滚轮能看到不同角度下正方体的形状,右键拖动能将最近的正方体顶点挪到这个投影面的相应位置。
按键控制。wasd 控制投影面旋转,ws 关于 x 轴旋转,ad 关于 y 轴旋转。
个人思路
首先投影面的确立需要两个量,一个 x 轴方向的单位向量,一个 y 轴方向的单位向量,求原点与三维空间中的点的连线到这两个单位向量的投影就能得到三维空间中的点在二维投影面中的坐标。记 x 轴方向单位向量为 X,y 轴方向单位向量为 Y,X 与 Y 互相垂直,模都为 1。
正方体的八个顶点位置随意,X,Y 两个单位向量只需是互相垂直的非零向量即可。
鼠标横向拖动时,X 关于 Y 旋转,这是怎么做到的呢。要做到这一点,就需要一个新的向量,也就是投影面的法向量,投影面的法向量可以根据两向量叉乘得到。叉乘的推法用的是线性代数的方法,但是不好理解,我用我的方法推出来,希望能方便理解。
设投影面法向量为 Z(x2, y2, z2),X(x0, y0, z0) 和 Y(x1, y1, z1) 与 Z 的点乘为 0,这就能列出两个式子:
① x0x2+y0y2+z0z2 = 0
② x1x2+y1y2+z1z2 = 0
将①式转化为以 x2 和 y2 表示的 z2 得
③ z2 = - (x0x2 + y0y2) / z0
将②式和③式结合转化为以 x2 表示的 y2 得
④ y2 = (x0z1 - x1z0) / (y1z0 - y0z1) * x2
④式代回③式得
⑤ z2 = (x1y0 - x0y1) / (y1z0 - y0z1) * x2 (推这一步时负号别忘了看)
也就是 Z = [x2, (x0z1 - x1z0) / (y1z0 - y0z1) * x2, (x1y0 - x0y1) / (y1z0 - y0z1) * x2]
仔细观察 Z 只有一个变量 x2,不妨先放大(y1z0 - y0z1)倍,得到
Z = [(y1z0 - y0z1) * x2, (x0z1 - x1z0) * x2, (x1y0 - x0y1) * x2]
把 x2 提取出来,Z 就是 Z = x2 * [(y1z0 - y0z1), (x0z1 - x1z0), (x1y0 - x0y1)]
令 x2 = 1,Z[(y1z0 - y0z1), (x0z1 - x1z0), (x1y0 - x0y1)] 就是投影面的法向量。到这一步还没有结束,因为垂直于一个面的法向量有两个,一个符合右手坐标系,一个符合左手坐标系,将 X(1, 0, 0),Y(0, 1, 0) 代入得 Z(0, 0, -1),所以这个 Z 是符合左手坐标系的投影面法向量,要转换成右手坐标系只需乘个 -1 就行,也就是 Z 最终为 (y0z1 - y1z0, x1z0 - x0z1, x0y1 - x1y0)。
由于 X,Y 都是单位向量,这个 Z 还是投影面的单位法向量。
Z 求出来了,X 关于 Y 旋转可以看做 X 在 XOZ 平面上旋转,问题转化成了求平面中某个向量转过θ度后的向量,如下图,将 X 看做下图中的红色向量,Z 看做下图中的绿色向量,虚线为向量旋转后θ度后的向量,可以发现 cos(θ)X - sin(θ)Z,就能求出 X 顺时针转动θ度后的向量,而 cos(θ)Z + sin(θ)X 就能求出 Z 顺时针转动θ度后的向量。
其它的旋转方式皆可以此类推。
代码实现
TCW_GUI.h:
#pragma once
#include<graphics.h>
#include<string>
#include<list>
#include<functional>
#define TCW_GUI_BUTTON_MYSELF 0
namespace TCW_GUI
{
enum class State
{
general = 0,
touch = 1,
press = 2,
release = 3,
forbidden = 4
};
class Vec2
{
public:
double x, y;
Vec2() :x(0), y(0) {}
Vec2(double xx, double yy) :x(xx), y(yy) {};
Vec2 operator+(Vec2 num)
{
return Vec2(x + num.x, y + num.y);
}
Vec2 operator-(Vec2 num)
{
return Vec2(x - num.x, y - num.y);
}
Vec2 operator/(double num)
{
return Vec2(x / num, y / num);
}
Vec2 operator*(double num)
{
return Vec2(x * num, y * num);
}
};
class Rect
{
public:
Rect() :size(), position() {}
Rect(Vec2 position, Vec2 size) :size(size), position(position) {}
Vec2 size;
Vec2 position;
bool isInRect(Vec2 point)
{
Vec2 left_top = position - size / 2.0;
Vec2 right_buttom = position + size / 2.0;
if (point.x >= left_top.x && point.y >= left_top.y &&
point.x <= right_buttom.x && point.y <= right_buttom.y)return true;
return false;
}
};
class Button
{
private:
double textsize = 20;
double textareasize = 0.9;
Vec2 defaultsize = Vec2(textwidth(L"...") / textareasize, textheight(L"...") / textareasize);
Vec2 defaulttext = Vec2(textwidth(L"..."), textheight(L"..."));
State nowstate = State::general;
void DrawButton_General();
void DrawButton_Touch();
void DrawButton_Press();
void DrawButton_Forbidden();
bool isPress = false;
public:
Button() :boundingbox(), buttontext() {}
Button(Rect boundingbox, std::wstring buttontext, std::function<int(void*)> releaseFunc, void* releaseParam) :
boundingbox(boundingbox), buttontext(buttontext), releaseFunc(releaseFunc), releaseParam(releaseParam) {}
std::wstring buttontext;
Rect boundingbox;
std::function<int(void*)> releaseFunc = nullptr;
void* releaseParam = nullptr;
void DrawButton();
void receiver(ExMessage* msg);
void ForbidButton() { this->nowstate = State::forbidden; } // 禁用按钮
void RefreshButton() { this->nowstate = State::general; } // 恢复按钮
void SetTextSize(double size)
{
textsize = size;
defaultsize = Vec2(textsize * 1.5 / textareasize, textsize / textareasize);
defaulttext = Vec2(textsize * 1.5, textsize);
}
void SetTextAreaSize(double size)
{
textareasize = size;
defaultsize = Vec2(textsize * 1.5 / textareasize, textsize / textareasize);
defaulttext = Vec2(textsize * 1.5, textsize);
}
};
class ButtonManager
{
std::list<Button> buttonlist;
public:
Button* AddButton(Button button);
void ReceiveMessage(ExMessage* msg);
void DrawButton();
};
void Rectangle_TCW(Vec2 left_top, Vec2 right_buttom)
{
rectangle(left_top.x, left_top.y, right_buttom.x, right_buttom.y);
}
void Fillrectangle_TCW(Vec2 left_top, Vec2 right_buttom)
{
fillrectangle(left_top.x, left_top.y, right_buttom.x, right_buttom.y);
}
void Outtextxy_TCW(Vec2 position, const WCHAR* str)
{
outtextxy(position.x, position.y, str);
}
void Button::DrawButton_General()
{
LOGFONT log;
COLORREF textcol;
COLORREF linecol;
COLORREF fillcol;
int bkmode;
gettextstyle(&log);
bkmode = getbkmode();
textcol = gettextcolor();
linecol = getlinecolor();
fillcol = getfillcolor();
settextstyle(textsize, 0, TEXT("微软雅黑"));
settextcolor(BLACK);
setbkmode(TRANSPARENT);
setlinecolor(BLACK);
setfillcolor(WHITE);
Vec2 size_button = Vec2(this->boundingbox.size * textareasize);
Vec2 size_text = Vec2(textwidth(this->buttontext.c_str()), textsize);
if (boundingbox.size.x > defaultsize.x && boundingbox.size.y > defaultsize.y)
{
Rectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
if (size_button.x >= size_text.x && size_button.y >= size_text.y)
{
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
}
else
{
int wordnum = size_button.x / textwidth(buttontext.c_str()) * buttontext.size() - 2;
std::wstring realstr = buttontext.substr(0, wordnum);
realstr += L"...";
size_text = Vec2(textwidth(realstr.c_str()), textsize);
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, realstr.c_str());
}
}
else
{
Rectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
if (defaulttext.x >= size_text.x && defaulttext.y >= size_text.y)
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
else Outtextxy_TCW(this->boundingbox.position - defaulttext / 2.0, TEXT("..."));
}
settextstyle(&log);
settextcolor(textcol);
setbkmode(bkmode);
setlinecolor(linecol);
setfillcolor(fillcol);
}
void Button::DrawButton_Touch()
{
LOGFONT log;
COLORREF textcol;
COLORREF linecol;
COLORREF fillcol;
int bkmode;
gettextstyle(&log);
bkmode = getbkmode();
textcol = gettextcolor();
linecol = getlinecolor();
fillcol = getfillcolor();
settextstyle(textsize, 0, TEXT("微软雅黑"));
settextcolor(BLACK);
setbkmode(TRANSPARENT);
setlinecolor(BLACK);
setfillcolor(RGB(240, 240, 240));
Vec2 size_button = Vec2(this->boundingbox.size * textareasize);
Vec2 size_text = Vec2(textwidth(this->buttontext.c_str()), textsize);
if (boundingbox.size.x > defaultsize.x && boundingbox.size.y > defaultsize.y)
{
Rectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
if (size_button.x >= size_text.x && size_button.y >= size_text.y)
{
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
}
else
{
int wordnum = size_button.x / textwidth(buttontext.c_str()) * buttontext.size() - 2;
std::wstring realstr = buttontext.substr(0, wordnum);
realstr += L"...";
size_text = Vec2(textwidth(realstr.c_str()), textsize);
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, realstr.c_str());
}
}
else
{
Rectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
if (defaulttext.x >= size_text.x && defaulttext.y >= size_text.y)
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
else Outtextxy_TCW(this->boundingbox.position - defaulttext / 2.0, TEXT("..."));
}
settextstyle(&log);
settextcolor(textcol);
setbkmode(bkmode);
setlinecolor(linecol);
setfillcolor(fillcol);
}
void Button::DrawButton_Press()
{
LOGFONT log;
COLORREF textcol;
COLORREF linecol;
COLORREF fillcol;
int bkmode;
gettextstyle(&log);
bkmode = getbkmode();
textcol = gettextcolor();
linecol = getlinecolor();
fillcol = getfillcolor();
settextstyle(textsize, 0, TEXT("微软雅黑")); // 设置字体为宽高 20 的字,有一些字体的中文宽度为字母的两倍
settextcolor(BLACK);
setbkmode(TRANSPARENT);
setlinecolor(BLACK);
setfillcolor(RGB(240, 240, 240));
Vec2 size_button = Vec2(this->boundingbox.size * textareasize);
Vec2 size_text = Vec2(textwidth(this->buttontext.c_str()), textsize);
if (boundingbox.size.x > defaultsize.x && boundingbox.size.y > defaultsize.y)
{
Rectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
if (size_button.x >= size_text.x && size_button.y >= size_text.y)
{
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0 + Vec2(2, 2), buttontext.c_str());
}
else
{
int wordnum = size_button.x / textwidth(buttontext.c_str()) * buttontext.size() - 2;
std::wstring realstr = buttontext.substr(0, wordnum);
realstr += L"...";
size_text = Vec2(textwidth(realstr.c_str()), textsize);
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, realstr.c_str());
}
}
else
{
Rectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
if (defaulttext.x >= size_text.x && defaulttext.y >= size_text.y)
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0 + Vec2(2, 2), buttontext.c_str());
else Outtextxy_TCW(this->boundingbox.position - defaulttext / 2.0 + Vec2(2, 2), TEXT("..."));
}
settextstyle(&log);
settextcolor(textcol);
setbkmode(bkmode);
setlinecolor(linecol);
setfillcolor(fillcol);
}
void Button::DrawButton_Forbidden()
{
LOGFONT log;
COLORREF textcol;
COLORREF linecol;
COLORREF fillcol;
int bkmode;
gettextstyle(&log);
bkmode = getbkmode();
textcol = gettextcolor();
linecol = getlinecolor();
fillcol = getfillcolor();
settextstyle(textsize, 0, TEXT("微软雅黑"));
settextcolor(RGB(128, 128, 128));
setbkmode(TRANSPARENT);
setlinecolor(BLACK);
setfillcolor(WHITE);
Vec2 size_button = Vec2(this->boundingbox.size * textareasize);
Vec2 size_text = Vec2(textwidth(this->buttontext.c_str()), textsize);
if (boundingbox.size.x > defaultsize.x && boundingbox.size.y > defaultsize.y)
{
Rectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->boundingbox.size / 2.0,
this->boundingbox.position + this->boundingbox.size / 2.0);
if (size_button.x >= size_text.x && size_button.y >= size_text.y)
{
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
}
else
{
int wordnum = size_button.x / textwidth(buttontext.c_str()) * buttontext.size() - 2;
std::wstring realstr = buttontext.substr(0, wordnum);
realstr += L"...";
size_text = Vec2(textwidth(realstr.c_str()), textsize);
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, realstr.c_str());
}
}
else
{
Rectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
Fillrectangle_TCW(this->boundingbox.position - this->defaultsize / 2.0,
this->boundingbox.position + this->defaultsize / 2.0);
if (defaulttext.x >= size_text.x && defaulttext.y >= size_text.y)
Outtextxy_TCW(this->boundingbox.position - size_text / 2.0, buttontext.c_str());
else Outtextxy_TCW(this->boundingbox.position - defaulttext / 2.0, TEXT("..."));
}
settextstyle(&log);
settextcolor(textcol);
setbkmode(bkmode);
setlinecolor(linecol);
setfillcolor(fillcol);
}
void Button::DrawButton()
{
switch (this->nowstate)
{
case State::general:
DrawButton_General();
break;
case State::touch:
DrawButton_Touch();
break;
case State::press:
DrawButton_Press();
break;
case State::release:
DrawButton_Touch();
if (releaseFunc != nullptr)
{
if (releaseParam == TCW_GUI_BUTTON_MYSELF)releaseFunc(this);
else releaseFunc(releaseParam);
}
this->nowstate = State::touch;
break;
case State::forbidden:
DrawButton_Forbidden();
break;
default:
break;
}
}
void Button::receiver(ExMessage* msg)
{
if (this->nowstate == State::forbidden)return;
// 先 general 后 touch 再 press 一个 release 后重新 general
if (!isPress && !this->boundingbox.isInRect(Vec2(msg->x, msg->y)))
{
this->nowstate = State::general;
}
else if (!isPress && this->boundingbox.isInRect(Vec2(msg->x, msg->y)))
{
if (!msg->lbutton)
this->nowstate = State::touch;
else if (this->nowstate == State::touch)
{
isPress = true;
this->nowstate = State::press;
}
}
else if (isPress && this->boundingbox.isInRect(Vec2(msg->x, msg->y)))
{
if (!msg->lbutton)
{
isPress = false;
this->nowstate = State::release;
}
else this->nowstate = State::press;
}
else if (isPress && !this->boundingbox.isInRect(Vec2(msg->x, msg->y)))
{
if (!msg->lbutton)
{
isPress = false;
this->nowstate = State::general;
}
else this->nowstate = State::press;
}
}
Button* ButtonManager::AddButton(Button button)
{
this->buttonlist.push_back(button);
return &buttonlist.back();
}
void ButtonManager::ReceiveMessage(ExMessage* msg)
{
for (Button& button : this->buttonlist)
{
button.receiver(msg);
}
}
void ButtonManager::DrawButton()
{
for (Button& button : this->buttonlist)
{
button.DrawButton();
}
}
}
main.cpp:
////////////////////////////////////////////
// 程序:一个正方体
// 作者:我想做三国志
// 编译环境:Visual Studio 2019,EasyX_20211109
// 编写日期:2022-8-30
#include<math.h>
#include<conio.h>
#include"TCW_GUI.h"
#define WIDTH 640 // 窗口宽度
#define HEIGHT 480 // 窗口高度
#define PI 3.14159265 // π
#define SIDE (min(WIDTH, HEIGHT) / 4) // 正方体边长
#define GAMEPAD (SIDE / 2) // 手柄,控制面旋转幅度的量
#define DISPLAY 3 // 展示出来顶点的尺寸
#define ARROWS 3 // 箭头尺寸
#define PIECE 360
double FocalLength = 6; // 观察点到投影面的距离
// 8 个顶点的颜色,用于分辨 8 个不同的点
COLORREF VertexColor[8] =
{
RED, YELLOW, BLUE, GREEN, BROWN, MAGENTA, CYAN, WHITE
};
struct Vec2
{
double x, y;
};
Vec2 operator*(Vec2 a, double num)
{
return { a.x * num, a.y * num };
}
Vec2 operator+(Vec2 a, Vec2 b)
{
return { a.x + b.x, a.y + b.y };
}
Vec2 operator-(Vec2 a, Vec2 b)
{
return { a.x - b.x, a.y - b.y };
}
double operator*(Vec2 a, Vec2 b)
{
return a.x * b.x + a.y * b.y;
}
Vec2 operator/(Vec2 a, double num)
{
return { a.x / num, a.y / num };
}
// 三维向量,也可以表示一个坐标
struct Vec3
{
double x, y, z;
};
typedef struct Vec3;
// 求两向量相减
Vec3 operator-(Vec3 a, Vec3 b)
{
return { a.x - b.x, a.y - b.y, a.z - b.z };
}
// 求两向量相加
Vec3 operator+(Vec3 a, Vec3 b)
{
return { a.x + b.x, a.y + b.y, a.z + b.z };
}
// 得到两向量点乘的值
double operator*(Vec3 a, Vec3 b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}
// 得到向量缩短 num 倍后的向量
Vec3 operator/(Vec3 a, long double num)
{
Vec3 result;
result.x = a.x / num;
result.y = a.y / num;
result.z = a.z / num;
return result;
}
// 得到向量延长 num 倍后的向量
Vec3 operator*(Vec3 a, long double num)
{
Vec3 result;
result.x = a.x * num;
result.y = a.y * num;
result.z = a.z * num;
return result;
}
// 得到一个向量的模长
double GetVec3Length(Vec3 vec)
{
return sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
}
// 得到向量 a 与向量 b 的夹角余弦值
double GetCosineOfTheAngle(Vec3 a, Vec3 b)
{
return a * b / GetVec3Length(a) / GetVec3Length(b);
}
// 得到向量 A 在向量 B 上的投影
Vec3 GetProjectionAOntoB(Vec3 A, Vec3 B)
{
double num = GetCosineOfTheAngle(A, B); // 得到向量 A,B 的夹角余弦值
double length = GetVec3Length(A) * num; // 向量 A 的模长乘 num 为向量 A 在向量 B 上投影的模长
Vec3 result = B * (abs(length) / GetVec3Length(B)); // 向量 B 延长 length 倍再缩短 B 的模长倍就是向量 A 在向量 B 上的投影
// 如果 length 比 0 小说明 num 小于 0,也就是两向量夹角大于 90 度,结果要变为相反向量
if (length > 0)return result;
return result * (-1.0);
}
// 根据投影面 x,y 轴正方向向量求出投影面法向量
Vec3 getVerticalAxis(Vec3 AuxiliaryVector[2])
{
double x0 = AuxiliaryVector[0].x;
double y0 = AuxiliaryVector[0].y;
double z0 = AuxiliaryVector[0].z;
double x1 = AuxiliaryVector[1].x;
double y1 = AuxiliaryVector[1].y;
double z1 = AuxiliaryVector[1].z;
Vec3 result = { y0 * z1 - y1 * z0, x1 * z0 - x0 * z1, x0 * y1 - x1 * y0 };
return result;
}
// 将三维的点的值转换为在对应 xoy 面上的投影的坐标
typedef Vec3 DoubleVec3[2];
Vec2 Transform3DTo2D(Vec3 vertex, DoubleVec3 AuxiliaryVector, bool isParallel)
{
Vec2 result;
Vec3 tempX = GetProjectionAOntoB(vertex, AuxiliaryVector[0]); // 得到三维向量在 x 轴上的投影
Vec3 tempY = GetProjectionAOntoB(vertex, AuxiliaryVector[1]); // 得到三维向量在 y 轴上的投影
result.x = GetVec3Length(tempX); // 得到 tempX 的模长,模长就是结果的 x 值的绝对值
result.y = GetVec3Length(tempY); // 得到 tempY 的模长,模长就是结果的 y 值的绝对值
if (tempX * AuxiliaryVector[0] < 0)result.x *= -1; // 如果 tempX 向量与 x 轴正方向的向量夹角大于 90 度,也就是向量点乘为负数,那么结果的 x 值为负数
if (tempY * AuxiliaryVector[1] < 0)result.y *= -1; // 如果 tempY 向量与 y 轴正方向的向量夹角大于 90 度,也就是向量点乘为负数,那么结果的 y 值为负数
if (isParallel)return result;
Vec3 Vec_Z = getVerticalAxis(AuxiliaryVector) * SIDE * FocalLength;
Vec3 target = vertex - Vec_Z;
return result * (SIDE * FocalLength / GetVec3Length(GetProjectionAOntoB(target, Vec_Z)));
}
// 画一个正方体
void drawCube(Vec3 Vertex[8], Vec3 AuxiliaryVector[2], Vec2 pericenter, bool isParallel)
{
Vec2 Temp[8];
for (int i = 0; i < 8; i++)
{
Vec2 temp = Transform3DTo2D(Vertex[i], AuxiliaryVector, isParallel);
Temp[i] = temp;
setfillcolor(VertexColor[i]);
solidcircle(temp.x + pericenter.x, temp.y + pericenter.y, DISPLAY);
}
line(Temp[0].x + pericenter.x, Temp[0].y + pericenter.y, Temp[3].x + pericenter.x, Temp[3].y + pericenter.y);
line(Temp[0].x + pericenter.x, Temp[0].y + pericenter.y, Temp[1].x + pericenter.x, Temp[1].y + pericenter.y);
line(Temp[0].x + pericenter.x, Temp[0].y + pericenter.y, Temp[4].x + pericenter.x, Temp[4].y + pericenter.y);
line(Temp[1].x + pericenter.x, Temp[1].y + pericenter.y, Temp[2].x + pericenter.x, Temp[2].y + pericenter.y);
line(Temp[1].x + pericenter.x, Temp[1].y + pericenter.y, Temp[5].x + pericenter.x, Temp[5].y + pericenter.y);
line(Temp[2].x + pericenter.x, Temp[2].y + pericenter.y, Temp[3].x + pericenter.x, Temp[3].y + pericenter.y);
line(Temp[2].x + pericenter.x, Temp[2].y + pericenter.y, Temp[6].x + pericenter.x, Temp[6].y + pericenter.y);
line(Temp[3].x + pericenter.x, Temp[3].y + pericenter.y, Temp[7].x + pericenter.x, Temp[7].y + pericenter.y);
line(Temp[4].x + pericenter.x, Temp[4].y + pericenter.y, Temp[5].x + pericenter.x, Temp[5].y + pericenter.y);
line(Temp[4].x + pericenter.x, Temp[4].y + pericenter.y, Temp[7].x + pericenter.x, Temp[7].y + pericenter.y);
line(Temp[5].x + pericenter.x, Temp[5].y + pericenter.y, Temp[6].x + pericenter.x, Temp[6].y + pericenter.y);
line(Temp[6].x + pericenter.x, Temp[6].y + pericenter.y, Temp[7].x + pericenter.x, Temp[7].y + pericenter.y);
char arr[128];
WCHAR ano[128];
settextstyle(0, 0, _T("Consolas"));
settextcolor(WHITE);
sprintf_s(arr, "x:(%f, %f, %f)", AuxiliaryVector[0].x, AuxiliaryVector[0].y, AuxiliaryVector[0].z);
MultiByteToWideChar(CP_ACP, 0, arr, -1, ano, 128);
outtextxy(10, HEIGHT / 6 * 5, ano);
sprintf_s(arr, "y:(%f, %f, %f)", AuxiliaryVector[1].x, AuxiliaryVector[1].y, AuxiliaryVector[1].z);
MultiByteToWideChar(CP_ACP, 0, arr, -1, ano, 128);
outtextxy(10, HEIGHT / 9 * 8, ano);
}
// 得到两个点之间的距离(二维)
double getTwoPointDistance(Vec2 a, Vec2 b)
{
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
// 得到 8 个顶点中距离 point 这个二维点最近的点,p 是判断两点间距离是否符合要求的
int getNearestIndex(Vec3 Vertex[8], Vec3 AuxiliaryVector[2], Vec2 point, bool isParallel, bool (*p)(double) = nullptr)
{
int result = 0;
double nearestDistance = getTwoPointDistance(Transform3DTo2D(Vertex[0], AuxiliaryVector, isParallel), point);
for (int i = 1; i < 8; i++)
{
double temp = getTwoPointDistance(Transform3DTo2D(Vertex[i], AuxiliaryVector, isParallel), point);
if (temp < nearestDistance)
{
result = i;
nearestDistance = temp;
}
}
if (p != nullptr && !p(nearestDistance))return -1;
return result;
}
void Line(Vec2 begin, Vec2 end)
{
line(begin.x, begin.y, end.x, end.y);
}
// grading 是粒度,一条线分为几份,0.1 是 10 份
void progressiveLine(Vec3 begincol, Vec3 endcol, Vec2 started, Vec2 finaled, double grading = 0.1)
{
Vec2 AddLine = (finaled - started) * grading;
Vec3 AddCol = (endcol - begincol) * grading;
Vec3 nowcol = begincol;
for (int i = 0; i < 1 / grading; i++)
{
nowcol = nowcol + AddCol;
setlinecolor(RGB(nowcol.x, nowcol.y, nowcol.z));
Line(started + AddLine * i, started + AddLine * (i + 1));
}
}
// 画坐标轴,pericenter 是中心点,size 是一个单位长度的长度
void drawCoordinateAxis(Vec2 pericenter, double size)
{
setlinestyle(PS_SOLID, 1);
setlinecolor(WHITE);
settextcolor(WHITE);
settextstyle(20, 0, _T("Consolas"));
double index_X = size * sqrt(3) / sqrt(5);
double index_Y = size * sqrt(2) / sqrt(5);
progressiveLine({ 0, 0, 0 }, { 255, 255, 255 }, pericenter + Vec2({ -index_X, -index_Y }),
pericenter + Vec2({ index_X, index_Y }));
line(pericenter.x + index_X - ARROWS, pericenter.y + index_Y, pericenter.x + index_X, pericenter.y + index_Y);
line(pericenter.x + index_X, pericenter.y + index_Y - ARROWS, pericenter.x + index_X, pericenter.y + index_Y);
outtextxy(pericenter.x + index_X, pericenter.y + index_Y, L"y");
progressiveLine({ 0, 0, 0 }, { 255, 255, 255 }, pericenter + Vec2({ index_X, -index_Y }),
pericenter + Vec2({ -index_X, +index_Y }));
line(pericenter.x - index_X + ARROWS, pericenter.y + index_Y, pericenter.x - index_X, pericenter.y + index_Y);
line(pericenter.x - index_X, pericenter.y + index_Y - ARROWS, pericenter.x - index_X, pericenter.y + index_Y);
outtextxy(pericenter.x - index_X, pericenter.y + index_Y, L"x");
progressiveLine({ 0, 0, 0 }, { 255, 255, 255 }, pericenter + Vec2({ 0, index_X }),
pericenter + Vec2({ 0, -index_X }));
line(pericenter.x + ARROWS, pericenter.y - index_X + ARROWS, pericenter.x, pericenter.y - index_X);
line(pericenter.x - ARROWS, pericenter.y - index_X + ARROWS, pericenter.x, pericenter.y - index_X);
outtextxy(pericenter.x, pericenter.y - index_X - 20, L"z");
}
// 画出辅助向量
void drawAuxiliaryVector(Vec3 AuxiliaryVector[2], Vec2 pericenter, double size, bool isParallel)
{
settextstyle(20, 0, _T("Consolas"));
Vec3 Auxiliary[2] = { {-1, 1, 0}, {-1, -1, 1} };
Auxiliary[0] = Auxiliary[0] / GetVec3Length(Auxiliary[0]);
Auxiliary[1] = Auxiliary[1] / GetVec3Length(Auxiliary[1]);
Vec2 temp[2];
temp[0] = Transform3DTo2D(AuxiliaryVector[0] * size, Auxiliary, isParallel); // x 轴
temp[1] = Transform3DTo2D(AuxiliaryVector[1] * size, Auxiliary, isParallel); // y 轴
double Cos_XX = GetCosineOfTheAngle(getVerticalAxis(Auxiliary), AuxiliaryVector[0]);
double Cos_YY = GetCosineOfTheAngle(getVerticalAxis(Auxiliary), AuxiliaryVector[1]);
if (Cos_XX < 0.0 && Cos_YY < 0.0)
{
setlinestyle(PS_SOLID, 2);
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_XX, 0, 0 }, pericenter,
pericenter + Vec2({ temp[0].x, -temp[0].y }));
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_YY, 0, 0 }, pericenter,
pericenter + Vec2({ temp[1].x, -temp[1].y }));
drawCoordinateAxis(pericenter, size);
}
else if (Cos_XX >= 0.0 && Cos_YY < 0.0)
{
setlinestyle(PS_SOLID, 2);
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_YY, 0, 0 }, pericenter,
pericenter + Vec2({ temp[1].x, -temp[1].y }));
drawCoordinateAxis(pericenter, size);
setlinestyle(PS_SOLID, 2);
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_XX, 0, 0 }, pericenter,
pericenter + Vec2({ temp[0].x, -temp[0].y }));
}
else if (Cos_XX < 0.0 && Cos_YY >= 0.0)
{
setlinestyle(PS_SOLID, 2);
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_XX, 0, 0 }, pericenter,
pericenter + Vec2({ temp[0].x, -temp[0].y }));
drawCoordinateAxis(pericenter, size);
setlinestyle(PS_SOLID, 2);
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_YY, 0, 0 }, pericenter,
pericenter + Vec2({ temp[1].x, -temp[1].y }));
}
else if (Cos_XX >= 0.0 && Cos_YY >= 0.0)
{
drawCoordinateAxis(pericenter, size);
setlinestyle(PS_SOLID, 2);
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_XX, 0, 0 }, pericenter,
pericenter + Vec2({ temp[0].x, -temp[0].y }));
progressiveLine({ 127, 0, 0 }, { 127 + 127 * Cos_YY, 0, 0 }, pericenter,
pericenter + Vec2({ temp[1].x, -temp[1].y }));
}
settextcolor(RED);
outtextxy(pericenter.x + temp[0].x, pericenter.y - temp[0].y, L"X");
outtextxy(pericenter.x + temp[1].x, pericenter.y - temp[1].y, L"Y");
setlinestyle(PS_SOLID, 1);
setlinecolor(WHITE);
}
// x 轴固定在 xoy 平面上,旋转 x 轴和 z 轴就能看到这个三维物体的所有角度!!!
int main()
{
bool isExit = false;
initgraph(WIDTH, HEIGHT);
BeginBatchDraw();
Vec3 AuxiliaryVector[2] = { { sqrt(2) / 2.0, sqrt(2) / 2.0, 0 },
{ -sqrt(3) / 3.0, sqrt(3) / 3, sqrt(3) / 3.0 } }; // 辅助向量,分别是 x 轴,y 轴的单位向量
bool isParallel = false;
TCW_GUI::Button* button_param[2];
TCW_GUI::ButtonManager manager;
TCW_GUI::Button* button_temp = manager.AddButton(TCW_GUI::Button(TCW_GUI::Rect(TCW_GUI::Vec2(WIDTH * 5 / 6.0, HEIGHT / 6.0),
TCW_GUI::Vec2(WIDTH / 7.0, HEIGHT / 7.0)), L"透视投影",
[&](void* param)
{
TCW_GUI::Button** button = (TCW_GUI::Button**)param;
if (isParallel)
{
button[0]->buttontext = L"透视投影";
button[1]->RefreshButton();
isParallel = false;
}
else
{
button[0]->buttontext = L"平行投影";
button[1]->ForbidButton();
isParallel = true;
}
return 0;
}, nullptr));
button_temp->releaseParam = button_param;
button_param[0] = button_temp;
button_param[1] = manager.AddButton(TCW_GUI::Button(TCW_GUI::Rect(TCW_GUI::Vec2(WIDTH * 5 / 6.0, HEIGHT / 3.0),
TCW_GUI::Vec2(WIDTH / 7.0, HEIGHT / 7.0)), L"透视距离",
[&](void* param)
{
WCHAR arr[128];
char ano[128];
InputBox(arr, 128, L"请输入透视距离(推荐 1~10, 可小数)");
WideCharToMultiByte(CP_UTF8, 0, arr, -1, ano, 128, NULL, FALSE);
sscanf(ano, "%lf", &FocalLength);
return 0;
}, nullptr));
manager.AddButton(TCW_GUI::Button(TCW_GUI::Rect(TCW_GUI::Vec2(WIDTH * 5 / 6.0, HEIGHT / 2.0),
TCW_GUI::Vec2(WIDTH / 7.0, HEIGHT / 7.0)), L"结束程序",
[](void* param)
{
bool* isExit = (bool*)param;
*isExit = true;
return 0;
}, &isExit));
Vec3 Vertex[8]; // 8 个顶点的坐标
// 初始化 8 个顶点,这 8 个顶点是固定的,可以改变为任意坐标值,我们只是从不同的角度看这 8 个顶点
Vertex[0] = { -GAMEPAD, -GAMEPAD, -GAMEPAD };
Vertex[1] = { GAMEPAD, -GAMEPAD, -GAMEPAD };
Vertex[2] = { GAMEPAD, GAMEPAD, -GAMEPAD };
Vertex[3] = { -GAMEPAD, GAMEPAD, -GAMEPAD };
Vertex[4] = { -GAMEPAD, -GAMEPAD, GAMEPAD };
Vertex[5] = { GAMEPAD, -GAMEPAD, GAMEPAD };
Vertex[6] = { GAMEPAD, GAMEPAD, GAMEPAD };
Vertex[7] = { -GAMEPAD, GAMEPAD, GAMEPAD };
ExMessage msg; // 鼠标信息
bool ispress = false; // 是否按下
bool isLpress = false; // 是否按下左键
bool isRpress = false; // 是否按下右键
double originalX = 0, originalY = 0; // 原来的坐标
int vertexIndex = 0; // 右键点击时要操作的顶点的坐标
while (!isExit)
{
while (peekmessage(&msg, EM_MOUSE))
{
if (!ispress && (msg.lbutton || msg.rbutton))
{
ispress = true;
if (msg.lbutton)isLpress = true; // 左键按下
else if (msg.rbutton) // 右键按下
{
isRpress = true;
vertexIndex = getNearestIndex(Vertex, AuxiliaryVector, // 得到距离按下的位置最近的正方体顶点的下标
{ (double)msg.x - WIDTH / 2, (double)msg.y - HEIGHT / 2 }, isParallel,
[](double num) {if (num < DISPLAY)return true; return false; }); // 这个 lambda 表达式是为了让点到的地方距离最近的正方体顶点距离小于展示出来正方体顶点的尺寸才能生效
}
originalX = msg.x;
originalY = msg.y;
}
else if (isLpress && msg.lbutton)
{
double DelFi = (msg.y - originalY) / 6 / GAMEPAD * PI;
double DelTh = (msg.x - originalX) / GAMEPAD / 6 * PI;
Vec3 tempVectorX = AuxiliaryVector[0];
Vec3 tempVectorY = AuxiliaryVector[1];
Vec3 tempVectorZ = getVerticalAxis(AuxiliaryVector);
AuxiliaryVector[0] = tempVectorX * cos(DelTh) + tempVectorZ * sin(DelTh); // 改变 x 轴向量
tempVectorZ = tempVectorZ * cos(DelTh) - tempVectorX * sin(DelTh);
AuxiliaryVector[1] = tempVectorY * cos(DelFi) + tempVectorZ * sin(DelFi); // 改变 y 轴向量
originalX = msg.x;
originalY = msg.y;
}
else if (isRpress && msg.rbutton && vertexIndex != -1)
{
double lengthX = msg.x - originalX; // 在投影面横坐标上移动的距离
double lengthY = msg.y - originalY; // 在投影面纵坐标上移动的距离
// 对于选中的顶点,它变为它自身的向量加上投影面上的向量
Vertex[vertexIndex] =
Vertex[vertexIndex] +
AuxiliaryVector[0] * lengthX / GetVec3Length(AuxiliaryVector[0]) +
AuxiliaryVector[1] * lengthY / GetVec3Length(AuxiliaryVector[1]);
originalX = msg.x;
originalY = msg.y;
}
else if (ispress && !msg.lbutton)
{
ispress = false;
isLpress = false;
isRpress = false;
}
else if (msg.wheel)
{
double DelTh = msg.wheel / 120.0 * PI / 60.0; // 滚动 120 度旋转 3 度
Vec3 tempVectorX = AuxiliaryVector[0];
Vec3 tempVectorY = AuxiliaryVector[1];
Vec3 tempVectorZ = getVerticalAxis(AuxiliaryVector);
AuxiliaryVector[0] = tempVectorX * cos(DelTh) + tempVectorY * sin(DelTh); // 改变 x 轴向量
AuxiliaryVector[1] = tempVectorY * cos(DelTh) - tempVectorX * sin(DelTh); // 改变 y 轴向量
}
}
// 用鼠标不能进行精密控制,在这里用 wasd 实现键盘控制
if (_kbhit())
{
// 按一下移动 3 度
double DelFi = 0, DelTh = 0;
Vec3 tempVectorX = AuxiliaryVector[0];
Vec3 tempVectorY = AuxiliaryVector[1];
Vec3 tempVectorZ = getVerticalAxis(AuxiliaryVector);
switch (_getch())
{
case 'w':DelFi -= PI / 60.0; break;
case 'a':DelTh += PI / 60.0; break;
case 's':DelFi += PI / 60.0; break;
case 'd':DelTh -= PI / 60.0; break;
default:
break;
}
AuxiliaryVector[0] = tempVectorX * cos(DelTh) + tempVectorZ * sin(DelTh); // 改变 x 轴向量
tempVectorZ = tempVectorZ * cos(DelTh) - tempVectorZ * sin(DelTh);
AuxiliaryVector[1] = tempVectorY * cos(DelFi) + tempVectorZ * sin(DelFi); // 改变 y 轴向量
}
cleardevice();
drawCube(Vertex, AuxiliaryVector, { WIDTH / 2.0, HEIGHT / 2.0 }, isParallel);
drawAuxiliaryVector(AuxiliaryVector, { WIDTH / 6 * 5, HEIGHT / 6 * 5 }, min(WIDTH, HEIGHT) / 9,
isParallel);
manager.ReceiveMessage(&msg);
manager.DrawButton();
FlushBatchDraw();
}
closegraph();
return 0;
}
添加评论
取消回复