助力软件开发企业降本增效 PHP / java源码系统,只需一次付费,代码终身使用! 广告
尝试用C++写一个简单的小模型案例Billiard,来模拟图形绘制和数学渲染。基于MicroSoft的Win10和VS2017开发,如果有时间,我会继续完善。同时因为规模小,暂时就命名为Billiard。 成品完整代码见百度网盘 terryzhk: ----------------------------------------------------------------------------------------------- 首先,我们自己搭建一个数学库,以供我们后续使用,整个数学库的结构如下: Vector:用来定义向量以及各种相关运算。 Matrix:用来定义矩阵以及各种相关运算。 MathUtil:用来构造数学工具,实现插值,仿射变换等。 LoadBitmap:我们使用Gdiplus.dll这个库来读取图片像素颜色信息。 Texture2D:把我们的2D图片进行纹理寻址,从而显示出来。 Vertex:自己定义一个简单的顶点着色器。 Math:包含我们的数学库以供方便使用。 1 Matrix.h 定义矩阵。 初始化,置0,相等,加法,减法,乘法,除法。 #pragma once #include <cmath> class MMatrix { public: union { float m[4][4]; struct { float _11; float _12; float _13; float _14; float _21; float _22; float _23; float _24; float _31; float _32; float _33; float _34; float _41; float _42; float _43; float _44; }; }; public: MMatrix() = default; MMatrix(float a1,float a2,float a3,float a4, float b1,float b2,float b3,float b4, float c1,float c2,float c3,float c4, float d1,float d2,float d3,float d4) { _11 = a1; _12 = a2; _13 = a3; _14 = a4; _21 = b1; _22 = b2; _23 = b3; _24 = b4; _31 = c1; _32 = c2; _33 = c3; _34 = c4; _41 = d1; _42 = d2; _43 = d3; _44 = d4; } MMatrix(const MMatrix& rhs) { for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) m[i][j] = rhs.m[i][j]; } MMatrix& operator= (const MMatrix& rhs) { for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) m[i][j] = rhs.m[i][j]; return *this; } public: // identify a matrix void Identity(); // get a 0 matrix void SetZero(); // = bool operator== (const MMatrix& rhs) const; // + MMatrix operator+ (const MMatrix& rhs) const; // - MMatrix operator- (const MMatrix& rhs) const; // * MMatrix operator* (const MMatrix& rhs) const; }; 1.2 Matrix.cpp 矩阵运算。 矩阵相等,加法,减法,乘法。 #include "MMatrix.h" void MMatrix::Identity() { _11 = 1.f; _12 = 0.f; _13 = 0.f; _14 = 0.f; _21 = 0.f; _22 = 1.f; _23 = 0.f; _24 = 0.f; _31 = 0.f; _32 = 0.f; _33 = 1.f; _34 = 0.f; _41 = 0.f; _42 = 0.f; _43 = 0.f; _44 = 1.f; } void MMatrix::SetZero() { for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) m[i][j] = 0.0f; } // M1 == M2 / left hand side = right hand side bool MMatrix::operator==(const MMatrix& rhs) const { for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) if (abs(m[i][j] - rhs.m[i][j]) >= 0.000001f) return false; return true; } //M1 = M2 + M3 MMatrix MMatrix::operator+(const MMatrix& rhs) const { MMatrix matrix; for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) matrix.m[i][j] = m[i][j] + rhs.m[i][j]; return matrix; } //M1 = M2 - M3 MMatrix MMatrix::operator-(const MMatrix& rhs) const { MMatrix matrix; for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) matrix.m[i][j] = m[i][j] - rhs.m[i][j]; return matrix; } //M1 = M2 * M3 MMatrix MMatrix::operator*(const MMatrix& rhs) const { MMatrix matrix; for (int i = 0; i < 4; ++i) for(int j = 0; j < 4; ++j) { matrix.m[j][i] = (m[j][0] * rhs.m[0][i]) + (m[j][1] * rhs.m[1][i]) + (m[j][2] * rhs.m[2][i]) + (m[j][3] * rhs.m[3][i]); } return matrix; } 2.1 Vector.h 定义向量。 向量求模长,归一化,点乘,叉乘,与矩阵,向量,数字运算。 自定义三维向量结构,二维向量结构。 #pragma once #include <cmath> #include "MMatrix.h" class MVector { public: float x; float y; float z; float w; //homogeneous coordinates,0->vector, 1->dot public: MVector() = default; MVector(float x1, float y1, float z1, float w1 = 0) :x(x1), y(y1), z(z1), w(w1) {} MVector(const MVector& rhs) :x(rhs.x), y(rhs.y), z(rhs.z), w(rhs.w) {} MVector& operator= (const MVector& rhs) { if (this == &rhs) return *this; x = rhs.x; y = rhs.y; z = rhs.z; w = rhs.w; return *this; } public: // length of the vector float Length() const; // normalize a vector MVector Normalize(); // dot product float Dot(MVector v) const; // cross product MVector Cross(MVector rhs) const; // vector1 == vector2 bool operator==(const MVector& rhs) const; // vertor * matrix MVector operator* (const MMatrix& rhs) const; // verctor * vector MVector operator* (const MVector& rhs) const; // vector * float MVector operator*(float factor) const; // + MVector operator+ (const MVector& rhs) const; // - MVector operator- (const MVector& rhs) const; // negate MVector operator-() const; }; class MFLOAT3 { public: float x; float y; float z; public: MFLOAT3() = default; MFLOAT3(float r,float b,float g):x(r),y(b),z(g){} MFLOAT3(const MFLOAT3& rhs) :x(rhs.x), y(rhs.y), z(rhs.z) {} MFLOAT3& operator= (const MFLOAT3& rhs) { if (this == &rhs) return *this; x = rhs.x; y = rhs.y; z = rhs.z; return *this; } }; // to show texture class MFLOAT2 { public: float u; float v; public: MFLOAT2() = default; MFLOAT2(float x, float y) :u(x), v(y) {} MFLOAT2(const MFLOAT2& rhs):u(rhs.u),v(rhs.v){} MFLOAT2& operator= (const MFLOAT2& rhs) { if (this == &rhs) return *this; u = rhs.u; v = rhs.v; return *this; } }; 2.2 Vector.cpp 向量运算很简单。 向量各种基础运算。 3.1 MathUtil.h 各种常用数学计算,如插值,转置,仿射变换等。 #pragma once #include "MVector.h" #include "MMatrix.h" #include "Vertex.h" #include <windows.h> #include <cmath> #include <vector> namespace MathUtil { extern const float PI; //线性插值 t位于[0,1] float Lerp(float x1, float x2, float t); //矢量插值 MVector Lerp(const MVector& v1, const MVector& v2, float t); //ZCFLOAT2 插值 MFLOAT2 Lerp(const MFLOAT2& v1, const MFLOAT2& v2, float t); //ZCFLOAT3插值 MFLOAT3 Lerp(const MFLOAT3& v1, const MFLOAT3& v2, float t); //VertexOut插值 VertexOut Lerp(const VertexOut& v1, const VertexOut& v2, float t); //clamp float Clamp(float x, float min, float max); //角度转弧度 inline float MConvertToRadians(float fDegrees) { return fDegrees * (PI / 180.0f); } //向量长度 float Length(const MVector& v); //单位矩阵 MMatrix MMatrixIdentity(); //矩阵转置 MMatrix MMatrixTranspose(const MMatrix& mat); //矩阵对应行列式 float MMatrixDet(const MMatrix& mat); //伴随矩阵中的每一项 3*3矩阵对应的行列式值 float MMatrixAdjElem( float a1, float a2, float a3, float b1, float b2, float b3, float c1, float c2, float c3); //伴随矩阵 代数余子式组成的矩阵的转置 MMatrix MMatrixAdj(const MMatrix& mat); //逆矩阵 = 伴随矩阵/(行列式值的绝对值) MMatrix MMatrixInverse(const MMatrix& mat); //缩放矩阵 MMatrix MMatrixScaling(float scaleX, float scaleY, float scaleZ); //平移矩阵 MMatrix MMatrixTranslate(float offsetX, float offsetY, float offsetZ); //绕x轴旋转矩阵 MMatrix MMatrixRotationX(float angle); //绕y轴旋转矩阵 MMatrix MMatrixRotationY(float angle); //绕z轴旋转矩阵 MMatrix MMatrixRotationZ(float angle); //获取视角矩阵 MMatrix MMatrixLookAtLH(MVector eyePos, MVector lookAt, MVector up); //获取投影矩阵 同dx中的XMMatrixPerspectiveFovLH // 观察角 宽高比 近裁剪平面 远裁剪平面 MMatrix MMatrixPerspectiveFovLH(float fovAngleY, float aspectRatio, float nearZ, float farZ); //投影出来的坐标到屏幕坐标变换矩阵 MMatrix MMatrixScreenTransform(int clientWidth, int clientHeight); //颜色ZCVector(r,b,g,a)转化为UINT UINT ColorToUINT(const MVector& color); //求入射向量关于法线的反射向量 MVector Reflect(const MVector& vin, const MVector& normal); } 3.2 MathUtil.cpp 插值:通过因子t使离散值逼近。 仿射变换:矩阵的基础运算。 #include "MathUtil.h" const float MathUtil::PI = 3.1415926f; //线性插值 t位于[0,1] inline float MathUtil::Lerp(float x1, float x2, float t) { return x1 + (x2 - x1)*t; } //矢量插值 MVector MathUtil::Lerp(const MVector& v1, const MVector& v2, float t) { return MVector( Lerp(v1.x, v2.x, t), Lerp(v1.y, v2.y, t), Lerp(v1.z, v2.z, t), v1.w ); } //ZCFLOAT2 插值 MFLOAT2 MathUtil::Lerp(const MFLOAT2& v1, const MFLOAT2& v2, float t) { return MFLOAT2( Lerp(v1.u, v2.u, t), Lerp(v1.v, v2.v, t) ); } //ZCFLOAT3 插值 MFLOAT3 MathUtil::Lerp(const MFLOAT3& v1, const MFLOAT3& v2, float t) { return MFLOAT3( Lerp(v1.x, v2.x, t), Lerp(v1.y, v2.y, t), Lerp(v1.z, v2.z, t) ); } //VertexOut 插值 VertexOut MathUtil::Lerp(const VertexOut& v1, const VertexOut& v2, float t) { return VertexOut( Lerp(v1.posTrans, v2.posTrans, t), Lerp(v1.posH, v2.posH, t), Lerp(v1.tex, v2.tex, t), Lerp(v1.normal, v2.normal, t), Lerp(v1.color, v2.color, t), Lerp(v1.oneDivZ, v2.oneDivZ, t) ); } //clamp float MathUtil::Clamp(float x, float min, float max) { if (x <= min) return min; else if (x >= max) return max; return x; } //向量长度 float MathUtil::Length(const MVector& v) { return sqrt(v.x*v.x + v.y*v.y + v.z*v.z); } //单位矩阵 MMatrix MathUtil::MMatrixIdentity() { return MMatrix(1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f); } //矩阵转置 MMatrix MathUtil::MMatrixTranspose(const MMatrix& mat) { return MMatrix(mat._11, mat._21, mat._31, mat._41, mat._12, mat._22, mat._32, mat._42, mat._13, mat._23, mat._33, mat._43, mat._14, mat._24, mat._34, mat._44); } //矩阵对应行列式 /************************************************************************/ /* a11a22a33a44 - a11a22a34a43 - a11a23a32a44 + a11a23a34a42 + a11a24a32a43 - a11a24a33a42 - a12a21a33a44 + a12a21a34a43 + a12a23a31a44 - a12a23a34a41 - a12a24a31a43 + a12a24a33a41 + a13a21a32a44 - a13a21a34a42 - a13a22a31a44 + a13a22a34a41 + a13a24a31a42 - a13a24a32a41 - a14a21a32a43 + a14a21a33a42 + a14a22a31a43 - a14a22a33a41 - a14a23a31a42 + a14a23a32a41 */ /************************************************************************/ float MathUtil::MMatrixDet(const MMatrix& mat) { float result = mat._11*mat._22*mat._33*mat._44 - mat._11*mat._22*mat._34*mat._43 - mat._11*mat._23*mat._32*mat._44 + mat._11*mat._23*mat._34*mat._42 + mat._11*mat._24*mat._32*mat._43 - mat._11*mat._24*mat._33*mat._42 - mat._12*mat._21*mat._33*mat._44 + mat._12*mat._21*mat._34*mat._43 + mat._12*mat._23*mat._31*mat._44 - mat._12*mat._23*mat._34*mat._41 - mat._12*mat._24*mat._31*mat._43 + mat._12*mat._24*mat._33*mat._41 + mat._13*mat._21*mat._32*mat._44 - mat._13*mat._21*mat._34*mat._42 - mat._13*mat._22*mat._31*mat._44 + mat._13*mat._22*mat._34*mat._41 + mat._13*mat._24*mat._31*mat._42 - mat._13*mat._24*mat._32*mat._41 - mat._14*mat._21*mat._32*mat._43 + mat._14*mat._21*mat._33*mat._42 + mat._14*mat._22*mat._31*mat._43 - mat._14*mat._22*mat._33*mat._41 - mat._14*mat._23*mat._31*mat._42 + mat._14*mat._23*mat._32*mat._41; return result; } //伴随矩阵中的每一项 3*3矩阵对应的行列式值 float MathUtil::MMatrixAdjElem( float a1, float a2, float a3, float b1, float b2, float b3, float c1, float c2, float c3) { return a1*(b2*c3 - c2*b3) - a2*(b1*c3 - c1*b3) + a3*(b1*c2 - c1*b2); } //伴随矩阵 代数余子式组成的矩阵的转置 MMatrix MathUtil::MMatrixAdj(const MMatrix& mat) { float a1 = MMatrixAdjElem(mat._22, mat._23, mat._24, mat._32, mat._33, mat._34, mat._42, mat._43, mat._44); float a2 = MMatrixAdjElem(mat._21, mat._23, mat._24, mat._31, mat._33, mat._34, mat._41, mat._43, mat._44); ...//简单的数学计算,此处可见源码。 MMatrix result( a1, -a2, a3, -a4, -b1, b2, -b3, b4, c1, -c2, c3, -c4, -d1, d2, -d3, d4 ); return MMatrixTranspose(result); } //逆矩阵 = 伴随矩阵/(行列式值的绝对值) MMatrix MathUtil::MMatrixInverse(const MMatrix& mat) { float det = abs(MMatrixDet(mat)); MMatrix adj = MMatrixAdj(mat); MMatrix inverse; for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) { inverse.m[i][j] = adj.m[i][j] / det; } return inverse; } //缩放矩阵 MMatrix MathUtil::MMatrixScaling(float scaleX, float scaleY, float scaleZ) { return MMatrix( scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1 ); } //平移矩阵 MMatrix MathUtil::MMatrixTranslate(float offsetX, float offsetY, float offsetZ) { return MMatrix( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, offsetX, offsetY, offsetZ, 1 ); } //绕x轴旋转矩阵 MMatrix MathUtil::MMatrixRotationX(float angle) { return MMatrix( 1, 0, 0, 0, 0, cos(angle), sin(angle), 0, 0, -sin(angle), cos(angle), 0, 0, 0, 0, 1 ); } //绕y轴旋转矩阵 MMatrix MathUtil::MMatrixRotationY(float angle) { return MMatrix( cos(angle), 0, -sin(angle), 0, 0, 1, 0, 0, sin(angle), 0, cos(angle), 0, 0, 0, 0, 1 ); } //绕z轴旋转矩阵 MMatrix MathUtil::MMatrixRotationZ(float angle) { return MMatrix( cos(angle), sin(angle), 0, 0, -sin(angle), cos(angle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); } //获取视角矩阵 MMatrix MathUtil::MMatrixLookAtLH(MVector eyePos, MVector lookAt, MVector up) { MVector zaxis = lookAt - eyePos; zaxis.Normalize(); MVector xaxis = up.Cross(zaxis).Normalize(); MVector yaxis = zaxis.Cross(xaxis); return MMatrix( xaxis.x, yaxis.x, zaxis.x, 0, xaxis.y, yaxis.y, zaxis.y, 0, xaxis.z, yaxis.z, zaxis.z, 0, -xaxis.Dot(eyePos), -yaxis.Dot(eyePos), -zaxis.Dot(eyePos), 1 ); } //获取投影矩阵 同dx中的XMMatrixPerspectiveFovLH // 观察角 宽高比 近裁剪平面 远裁剪平面 MMatrix MathUtil::MMatrixPerspectiveFovLH(float fovAngleY, float aspectRatio, float nearZ, float farZ) { MMatrix mat; mat.SetZero(); // tan(fovAngleY*0.5f) float height = cos(fovAngleY*0.5f) / sin(fovAngleY*0.5f); mat._11 = height / aspectRatio; mat._22 = height; mat._33 = farZ / (farZ - nearZ); mat._34 = 1.f; mat._43 = (nearZ * farZ) / (nearZ - farZ); return mat; } MMatrix MathUtil::MMatrixScreenTransform(int clientWidth, int clientHeight) { return MMatrix( clientWidth / 2, 0, 0, 0, 0, clientHeight / 2, 0, 0, 0, 0, 1, 0, clientWidth / 2, clientHeight / 2, 0, 1 ); } //颜色ZCFloat3(r,b,g,a)转化为UINT UINT MathUtil::ColorToUINT(const MVector& color) { BYTE red = 255 * color.x/* color.w*/; BYTE green = 255 * color.y/* color.w*/; BYTE blue = 255 * color.z /* color.w*/; return (UINT)((BYTE)blue | (WORD)((BYTE)green << 8) | (DWORD)((BYTE)red << 16)); } //求反射向量 MVector MathUtil::Reflect(const MVector& I, const MVector& N) { //公式 R = I - 2(I·N)N float tmp = 2.f * I.Dot(N); return I - (N * tmp); } 4.1 Texture2D.h 读取图片,深拷贝。 #pragma once #include <windows.h> #include "MVector.h" class Texture2D { public: Texture2D() = default; Texture2D(UINT width,UINT height); ~Texture2D(); Texture2D(const Texture2D& rhs) :m_width(rhs.m_width), m_height(rhs.m_height) { // deep copy m_pixelBuffer = new MVector*[m_width]; for (int i = 0; i < m_width; ++i) { m_pixelBuffer[i] = new MVector[m_height]; } for (int i = 0; i < m_width; ++i) { for (int j = 0; j < m_height; ++j) { m_pixelBuffer[i][j] = rhs.m_pixelBuffer[i][j]; } } } Texture2D& operator=(const Texture2D& rhs) { if (this == &rhs) return *this; m_width = rhs.m_width; m_height = rhs.m_height; m_pixelBuffer = new MVector*[m_width]; for (int i = 0; i < m_width; ++i) { m_pixelBuffer[i] = new MVector[m_height]; } for (int i = 0; i < m_width; ++i) { for (int j = 0; j < m_height; ++j) { m_pixelBuffer[i][j] = rhs.m_pixelBuffer[i][j]; } } return *this; } public: MVector Sample(const MFLOAT2& tex); public: UINT m_width; UINT m_height; MVector** m_pixelBuffer; }; 4.2 Texture.cpp 纹理寻址,左手坐标系。 #include "Texture2D.h" Texture2D::Texture2D(UINT width, UINT height) { m_width = width; m_height = height; m_pixelBuffer = new MVector*[width]; for (int i = 0; i < width; ++i) { m_pixelBuffer[i] = new MVector[height]; } } Texture2D::~Texture2D() { if (m_pixelBuffer) { for (int i = 0; i < m_width; ++i) { delete[] m_pixelBuffer[i]; } } } MVector Texture2D::Sample(const MFLOAT2& tex) { //纹理寻址采用d3d中的wrap方式 当坐标大于1时,通过去掉整数部分,根据得到的小数部分来得到纹理值; //坐标小于0,则加上一个最小正数,让坐标大于0。 if (tex.u >= 0 && tex.u <= 1 && tex.v >= 0 && tex.v <= 1) { UINT x = tex.u * (m_width - 1); UINT y = tex.v * (m_height - 1); return m_pixelBuffer[x][y]; } else { float u, v; if (tex.u > 1) u = tex.u - static_cast<int>(tex.u); else if (tex.u < 0) u = (static_cast<int>(-tex.u) + 1) + tex.u; if (tex.v > 1) v = tex.v - static_cast<int>(tex.v); else if (tex.v < 0) v = (static_cast<int>(-tex.v) + 1) + tex.v; UINT x = u * (m_width - 1); UINT y = v * (m_height - 1); return m_pixelBuffer[x][y]; } } 5.1 LoadBitmap.cpp 加载位图,取得像素颜色。 #include "LoadBitmap.h" #include <windows.h> #include <gdiplus.h> #include <iostream> #include <fstream> #include <sstream> #pragma comment(lib, "gdiplus.lib") using namespace std; using namespace Gdiplus; Texture2D MathUtil::LoadBitmapToColorArray(wstring filePath) { GdiplusStartupInput gdiplusstartupinput; ULONG_PTR gdiplustoken; GdiplusStartup(&gdiplustoken, &gdiplusstartupinput, nullptr); Bitmap* bmp = new Bitmap(filePath.c_str()); if (!bmp) { MessageBox(nullptr, "error", "picture path is null!", MB_OK); delete bmp; GdiplusShutdown(gdiplustoken); return Texture2D(0,0); } else { UINT height = bmp->GetHeight(); UINT width = bmp->GetWidth(); //初始化Texture2D Texture2D texture(width, height); Color color; for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) { bmp->GetPixel(x, y, &color); texture.m_pixelBuffer[x][height - 1 - y] = MVector( color.GetRed() / 255.f, color.GetGreen() / 255.f, color.GetBlue() / 255.f, 1.f ); } delete bmp; GdiplusShutdown(gdiplustoken); return texture; } } 6.1 Vertex.h 自定义的定点着色器,含有位置,法线,贴图等信息。 自定义结构:VertexIn经过vertex shader后输出VertexOut。 #pragma once #include "MVector.h" //info of a vertex class VertexIn { public: // position MVector pos; // color MVector color; // texture MFLOAT2 tex; // normal MVector normal; VertexIn() = default; VertexIn(MVector pos, MVector color, MFLOAT2 tex, MVector normal) :pos(pos), color(color), tex(tex), normal(normal) {} VertexIn(const VertexIn& rhs):pos(rhs.pos),color(rhs.color),tex(rhs.tex),normal(rhs.normal){} }; // VertexIn -> vertex shader -> VertexOut class VertexOut { public: // pos -> world transformation -> posTrans MVector posTrans; // pos -> projection transformation -> posH MVector posH; // texture MFLOAT2 tex; // normal MVector normal; // color MVector color; // 1/z depth testing float oneDivZ; VertexOut() = default; VertexOut(MVector posT, MVector posH, MFLOAT2 tex, MVector normal, MVector color, float oneDivZ) :posTrans(posT), posH(posH), tex(tex), normal(normal), color(color), oneDivZ(oneDivZ) {} VertexOut(const VertexOut& rhs) :posTrans(rhs.posTrans), posH(rhs.posH), tex(rhs.tex), normal(rhs.normal), color(rhs.color),oneDivZ(rhs.oneDivZ){} VertexOut& operator= (const VertexOut& rhs) { if (this == &rhs) return *this; this->normal = rhs.normal; this->posH = rhs.posH; this->posTrans = rhs.posTrans; this->tex = rhs.tex; this->color = rhs.color; this->oneDivZ = rhs.oneDivZ; return *this; } }; 暂时先把可能用到的各部分数学结构写好,方便我们以后使用。 有的朋友可能会有疑问,数学工具浩若烟海,为啥要重新造轮子呢? 我们当然知道使用Mathmatica,Matlab,Maple,Telax等软件的方便了…… 如果能用上述软件运行出结果何乐不为? 但是科学模拟和数学试验有时就需要科学工作者和数学家去DIY。 重新造好轮子就是逼不得已和饶有趣味的事了。