最近在使用Qt+OpenGL+glew+freeimage实现一个简单的场景编辑器,先看看他的功能是什么。


这个是软件的界面,中间的widget是用来显示场景的,左侧上方的dockwidget可以拖动模型到显示场景中,左侧下方是dockwidget是用于显示场景中加载的模型列表,右侧的dockwidget是用于显示当前模型的信息,包括他的位置和缩放比例。选中模型后会显示他的包围盒,左侧上方点击scene Action可以切换模式,是拖动场景还是拖动模型,拖动模型的时候会将所有显示包围盒的模型都进行移动。
界面模块
界面是使用QMainWindow,渲染界面是继承于QWidget,左右两侧有三个QDockWidget,左侧上方是一个QListWidget,用于显示所有xlm模型,左侧下方是一个QTreeWidget,用于显示所有在场景中的模型列表,右侧是用于显示模型的属性,这个是从Qt源码中拿出来的QtTreePropertyBrowser类。
QtListWidget.h
#ifndef QTLISTWIDGET_H
#define QTLISTWIDGET_H
#include <QListWidget>
#include "rapidxml.hpp"
class QtListWidget : public QListWidget
{
Q_OBJECT
public:
QtListWidget(QWidget *parent = 0);
virtual ~QtListWidget();
virtual void load(const char* xmlFile);
protected:
void dragEnterEvent(QDragEnterEvent *event);
void dragMoveEvent(QDragMoveEvent *event);
void dropEvent(QDropEvent *event);
void startDrag(Qt::DropActions supportedActions);
};
#endif
QtListWidget.cpp
#include <QtGui>
#include "QtListWidget.h"
QtListWidget::QtListWidget(QWidget *parent)
: QListWidget(parent)
{
setDragEnabled(true);
setViewMode(QListView::IconMode);
setSpacing(10);
setAcceptDrops(false);
setDropIndicatorShown(true);
//for (int i = 0; i < 10; ++i)
//{
// QString path = QCoreApplication::applicationDirPath();
// QPixmap bmp;
// bmp.load(path + "/images/example.jpg");
// QListWidgetItem* item = new QListWidgetItem(this);
// item->setIcon(QIcon(bmp));
// item->setData(Qt::UserRole, QVariant(bmp));
// item->setData(Qt::UserRole + 1, QPoint(1, 1));
// item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
// item->setText("test-icon");
//}
}
QtListWidget::~QtListWidget()
{
}
void QtListWidget::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("image/x-puzzle-piece"))
{
event->accept();
}
else
{
event->ignore();
}
}
void QtListWidget::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("image/x-puzzle-piece"))
{
event->setDropAction(Qt::MoveAction);
event->accept();
}
else
{
event->ignore();
}
}
void QtListWidget::dropEvent(QDropEvent *event)
{
event->ignore();
}
void QtListWidget::startDrag(Qt::DropActions /*supportedActions*/)
{
QListWidgetItem *item = currentItem();
QByteArray itemData;
QDataStream dataStream(&itemData, QIODevice::WriteOnly);
QPixmap pixmap = qvariant_cast<QPixmap>(item->data(Qt::UserRole));
QString location = item->data(Qt::UserRole + 1).toString();
dataStream << location;
QMimeData* mimeData = new QMimeData;
mimeData->setData("image/x-puzzle-piece", itemData);
QDrag* drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setHotSpot(QPoint(pixmap.width()/2, pixmap.height()/2));
drag->setPixmap(pixmap);
drag->exec(Qt::MoveAction);
}
void QtListWidget::load( const char* xmlFile )
{
FILE* pFile = fopen(xmlFile,"rb");
if (pFile == 0)
{
return;
}
fseek(pFile,0,SEEK_END);
size_t len = ftell(pFile);
fseek(pFile,0,SEEK_SET);
char* xml = new char[len + 1];
fread(xml,1,len,pFile);
xml[len] = 0;
fclose(pFile);
try
{
rapidxml::xml_document<> doc;
doc.parse<0>(xml);
rapidxml::xml_node<>* root = doc.first_node();
rapidxml::xml_node<>* model = root->first_node();
for (; model != 0; model = model->next_sibling())
{
rapidxml::xml_attribute<>* attrIcon = model->first_attribute("icon");
rapidxml::xml_attribute<>* attrFile = model->first_attribute("file");
rapidxml::xml_attribute<>* attrText = model->first_attribute("name");
QString bmpPath = QCoreApplication::applicationDirPath() + "/" + attrIcon->value();
QPixmap bmp;
bmp.load(bmpPath);
QListWidgetItem* item = new QListWidgetItem(this);
item->setIcon(QIcon(bmp));
item->setData(Qt::UserRole, QVariant(bmp));
item->setData(Qt::UserRole + 1, attrFile->value());
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
item->setText(attrText->value());
}
}
catch (...)
{
}
delete []xml;
}
渲染模块
渲染模块主要是通过glew库和Qt的定时器来实现的。
connect(&_renderTimer,&QTimer::timeout,this, &OGLWidget::render);
这个项目加载的模型是用的xml格式。
目前这个项目使用的固定管线来进行渲染的,所有没有写着色器。首先说一下记载纹理是用的FreeImage库
unsigned Scene::createTextureFromImage( const char* fileName )
{
//1 获取图片格式
FREE_IMAGE_FORMAT fifmt = FreeImage_GetFileType(fileName, 0);
if (fifmt == FIF_UNKNOWN)
{
return 0;
}
//2 加载图片
FIBITMAP* dib = FreeImage_Load(fifmt, fileName,0);
FREE_IMAGE_COLOR_TYPE type = FreeImage_GetColorType(dib);
//! 获取数据指针
FIBITMAP* temp = dib;
dib = FreeImage_ConvertTo32Bits(dib);
FreeImage_Unload(temp);
BYTE* pixels = (BYTE*)FreeImage_GetBits(dib);
int width = FreeImage_GetWidth(dib);
int height = FreeImage_GetHeight(dib);
for (int i = 0 ;i < width * height * 4 ; i+=4 )
{
BYTE temp = pixels[i];
pixels[i] = pixels[i + 2];
pixels[i + 2] = temp;
}
unsigned res = createTexture(width,height,pixels);
FreeImage_Unload(dib);
return res;
}
unsigned Scene::createTexture( int w,int h,const void* data )
{
unsigned texId;
glGenTextures(1,&texId);
glBindTexture(GL_TEXTURE_2D,texId);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_UNSIGNED_BYTE,data);
return texId;
}
V3U2 vertexs[] =
{
{-1, 0, 1, 0, 0},
{ 1, 0, 1, 1, 0},
{ 1, 0, -1, 1, 1},
{-1, 0, 1, 0, 0},
{ 1, 0, -1, 1, 1},
{-1, 0, -1, 0, 1},
};
for (int i = 0 ;i < 6 ; ++ i)
{
vertexs[i].x *= 100;
vertexs[i].z *= 100;
vertexs[i].u *= 10;
vertexs[i].v *= 10;
}
glBindTexture(GL_TEXTURE_2D,_texGround);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glVertexPointer(3,GL_FLOAT,sizeof(V3U2),&vertexs[0].x);
glTexCoordPointer(2,GL_FLOAT,sizeof(V3U2),&vertexs[0].u);
glDrawArrays(GL_TRIANGLES, 0, 6 );
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
这个是渲染地面的代码,只需要传入顶点数据,纹理坐标和纹理即可。
再说一下模型的渲染在当前项目中的架构:
首先有有一个基类CELLObject,里面记录了名称和一些用户自定义的数据。
#pragma once
namespace CELL
{
class CELLObject
{
protected:
void* _user;
char _name[64];
public:
CELLObject(void* user = 0)
:_user(user)
{}
virtual ~CELLObject()
{}
void setUserData(void* user)
{
_user = user;
}
void* getUserData()
{
return _user;
}
char* getName()
{
return _name;
}
void setName(char* name)
{
strncpy(_name, name, sizeof(_name));
}
};
}
负责管理节点的是CELLNode类,里面存储了模型的一些信息,包括位置,缩放,旋转信息,包围盒,可绘制体。
#pragma once
#include "CELLMath.hpp"
#include "CELLRenderable.hpp"
#include "glew/glew.h"
namespace CELL
{
class CELLNode :public CELLObject
{
public:
enum
{
FLAG_NORMAL = 1 << 0,
FLAG_SELECT = 1 << 1,
FLAG_VISIBLE = 1 << 2,
FLAG_UPDATE = 1 << 3,
};
public:
CELL::quatr _quat;
CELL::real3 _scale;
CELL::real3 _trans;
CELL::aabb3d _aabb;
CELL::matrix4 _local;
CELLRenderable* _renderable;
unsigned _flag;
public:
CELLNode()
{
_flag = FLAG_NORMAL | FLAG_VISIBLE;
_scale = float3(1,1,1);
_quat = angleAxis(real(60),real3(0,1,0));
_local = makeTransform(_trans,_scale,_quat);
sprintf(_name, "CELLNode-%p", this);
}
virtual ~CELLNode()
{}
inline void setFlag(unsigned flag)
{
_flag |= flag;
}
inline void removeFlag(unsigned flag)
{
_flag &= ~flag;
}
inline bool hasFlag(unsigned flag)
{
return _flag & flag ? true : false;
}
inline void setAabb(const aabb3d& aabb)
{
_aabb = aabb;
}
inline aabb3d getAabb()
{
return _aabb;
}
inline CELL::matrix4 getLocal()
{
return _local;
}
inline void setLocal(CELL::matrix4 matrix)
{
_local = matrix;
}
inline void setScale(const float3& scale)
{
_scale = scale;
}
inline float3 getScale()
{
return _scale;
}
inline float3 getTrans()
{
return _trans;
}
inline void setTranslation(const float3& trans)
{
_trans = trans;
}
inline void setQuat(const quatr& quat)
{
_quat = quat;
}
inline quatr getQuat()const
{
return _quat;
}
inline void setAngle(float angle,float3 axis = float3(0,1,0))
{
_quat = CELL::angleAxis(angle,axis);
}
inline void attach(CELLRenderable* render)
{
_renderable = render;
}
inline CELL::CELLRenderable* getAttach()const
{
return _renderable;
}
inline void update()
{
_local = makeTransform(_trans, _scale, _quat);
}
inline void render(CELLCamera& camera)
{
CELL::matrix4 vp = camera.getProject() * camera.getView();
CELL::matrix4 mvp = vp * _local;
glLoadMatrixf(mvp.data());
_renderable->render(this);
if(_flag & FLAG_SELECT)
renderAabb(vp);
}
inline void renderAabb(CELL::matrix4 &vp)
{
float3 vertex[8];
aabb3d aabb = _aabb;
aabb.transform(_local);
aabb.getAllCorners(vertex);
unsigned char index[] =
{
0,1, 1,2, 2,3, 3,0,
4,5, 5,6, 6,7, 7,4,
1,5, 2,4, 0,6, 3,7,
};
glLoadMatrixf(vp.data());
glBindTexture(GL_TEXTURE_2D, 0);
glColor3f(1, 0, 0);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, sizeof(float3), vertex);
glDrawElements(GL_LINES, sizeof(index), GL_UNSIGNED_BYTE, index);
glDisableClientState(GL_VERTEX_ARRAY);
glColor3f(1, 1, 1);
}
};
}
最后负责渲染的是CELLRenderable类,其中有两个虚函数需要子类实现
virtual bool load(const char* fileName)
{
return false;
}
/**
* 绘制函数
*/
virtual void render(CELLNode*)
{}
这两个函数分别是加载函数和渲染函数,由于目前是使用xml格式的数据进行渲染,所以实现了CELLModelXML类。
#pragma once
#include <vector>
#include "rapidxml.hpp"
#include "CELLRenderable.hpp"
#include "CELLFileReader.hpp"
#include "CELLTextureMgr.hpp"
#include <QString>
#include <QCoreApplication>
namespace CELL
{
class CELLModelXML :public CELLRenderable
{
public:
struct V3N3U2
{
float x,y,z;
float nx,ny,nz;
float u,v;
};
struct Face
{
short x,y,z;
};
struct Primitive
{
int start;
int count;
int mode;
};
typedef std::vector<V3N3U2> ArrayVertex;
typedef std::vector<Face> ArrayFace;
typedef std::vector<Primitive> ArrayPri;
private:
CELLModelXML(const CELLModelXML&);
CELLModelXML& operator = (const CELLModelXML& rights);
public:
ArrayVertex _vertexs;
ArrayFace _faces;
ArrayPri _primitive;
unsigned _texture;
public:
CELLModelXML()
{}
virtual ~CELLModelXML()
{}
/**
* 加载文件
*/
virtual bool load(const char* fileName)
{
CELLFileReader file(fileName);
if (file.isBad())
{
return false;
}
file.readAll();
try
{
rapidxml::xml_document<> doc;
rapidxml::xml_node<>* rootNode = 0;
char* xmlData = (char*)file.data();
doc.parse<0>(xmlData);
rootNode = doc.first_node("mesh");
if (rootNode == 0)
{
return false;
}
rapidxml::xml_node<>* node = rootNode->first_node();
for ( ; node != 0 ; node = node->next_sibling())
{
if(strcmp(node->name(),"vertex") == 0)
{
parseVertexs(node);
}
else if(strcmp(node->name(),"face") == 0)
{
parseFace(node);
}
else if(strcmp(node->name(),"primitive") == 0)
{
parsePrimitive(node);
}
}
return true;
}
catch (...)
{
return false;
}
}
virtual void render(CELLNode*)
{
glBindTexture(GL_TEXTURE_2D, _texture);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glVertexPointer(3,GL_FLOAT, sizeof(V3N3U2), &_vertexs[0].x);
glTexCoordPointer(2,GL_FLOAT, sizeof(V3N3U2), &_vertexs[0].u);
glDrawElements(GL_TRIANGLES,_faces.size() * 3,GL_UNSIGNED_SHORT,&_faces[0]);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}
protected:
/**
* 内部调用,主要用来计算包围盒大小
*/
void updateAabb()
{
float3 vMin = float3(FLT_MAX,FLT_MAX,FLT_MAX);
float3 vMax = float3(-FLT_MAX,-FLT_MAX,-FLT_MAX);
for (size_t i = 0 ;i < _vertexs.size() ; ++i)
{
V3N3U2& pData = _vertexs[i];
vMin.x = tmin(pData.x,vMin.x);
vMin.y = tmin(pData.y,vMin.y);
vMin.z = tmin(pData.z,vMin.z);
vMax.x = tmax(pData.x,vMax.x);
vMax.y = tmax(pData.y,vMax.y);
vMax.z = tmax(pData.z,vMax.z);
}
_aabb.setExtents(vMin,vMax);
}
/**
* 解析顶点信息
*/
virtual void parseVertexs( rapidxml::xml_node<>* node )
{
rapidxml::xml_attribute<>* attrCount = node->first_attribute("count");
rapidxml::xml_attribute<>* attrSizeOf = node->first_attribute("size");
rapidxml::xml_node<>* cNode = 0;
const char* pValue = 0;
unsigned dataSize = 0;
if (attrCount == 0 || attrSizeOf == 0)
{
return;
}
int dataCount = atoi(attrCount->value());
if (dataCount == 0)
{
return;
}
_vertexs.resize(dataCount);
cNode = node->first_node();
for ( int i = 0; cNode && i < dataCount; cNode = cNode->next_sibling(), ++ i)
{
V3N3U2& vertex = _vertexs[i];
pValue = cNode->value();
sscanf(
pValue
,"%f %f %f %f %f %f %f %f"
,&vertex.x
,&vertex.y
,&vertex.z
,&vertex.nx
,&vertex.ny
,&vertex.nz
,&vertex.u
,&vertex.v
);
}
updateAabb();
}
/**
* 解析面信息
*/
virtual void parseFace( rapidxml::xml_node<>* node )
{
rapidxml::xml_attribute<>* attrType = node->first_attribute("type");
rapidxml::xml_attribute<>* attrCount = node->first_attribute("count");
rapidxml::xml_attribute<>* attrTexture = node->first_attribute("texture");
rapidxml::xml_node<>* cNode = node->first_node();
if (attrType == 0 || attrCount== 0 || cNode == 0)
{
return;
}
int dataCount = atoi(attrCount->value());
if (dataCount == 0)
{
return;
}
if (attrTexture)
{
const char* pTextureFile = attrTexture->value();
QString path = QCoreApplication::applicationDirPath() + "/" + pTextureFile;
_texture = CELLTextureMgr::instance().getTexture(path.toLocal8Bit());
}
_faces.resize(dataCount);
for ( int i = 0 ; cNode && i < dataCount ; cNode = cNode->next_sibling(), ++ i)
{
int r,g,b;
Face& face = _faces[i];
sscanf(cNode->value(),"%d %d %d",&r,&g,&b);
face.x = (unsigned short)(r);
face.y = (unsigned short)(g);
face.z = (unsigned short)(b);
}
}
/**
* 解析绘制的图元组
*/
virtual void parsePrimitive( rapidxml::xml_node<>* node )
{
rapidxml::xml_attribute<>* attrCount = node->first_attribute("count");
rapidxml::xml_node<>* cNode = node->first_node();
if (attrCount == 0 || cNode == 0)
{
return;
}
int priCount = atoi(attrCount->value());
_primitive.resize(priCount);
for (int i = 0 ;cNode; cNode = cNode->next_sibling(), ++ i )
{
rapidxml::xml_attribute<>* attType = cNode->first_attribute("type");
rapidxml::xml_attribute<>* attStart = cNode->first_attribute("start");
rapidxml::xml_attribute<>* attCount = cNode->first_attribute("count");
if (attType == 0 || attStart == 0 || attCount == 0)
{
continue;
}
_primitive[i].mode = atoi(attType->value());
_primitive[i].start = atoi(attStart->value());
_primitive[i].count = atoi(attCount->value());
}
}
};
}
这个类的作用是从xml文件读取出顶点数据,纹理坐标数据,纹理路径,顶点索引数据,根据这些信息进行渲染。
交互模块
交互模块主要指的是用户操作后鼠标,键盘事件后场景进行对应的操作。
virtual void dragEnterEvent(QDragEnterEvent *evt);
virtual void dragMoveEvent(QDragMoveEvent *evt);
virtual void dropEvent(QDropEvent *evt);
/**
* 窗口大小变化事件
*/
virtual void resizeEvent(QResizeEvent *evt);
/**
* 鼠标按下
*/
virtual void mousePressEvent(QMouseEvent*evt);
virtual void mouseReleaseEvent(QMouseEvent*evt);
virtual void mouseDoubleClickEvent(QMouseEvent*evt);
virtual void mouseMoveEvent(QMouseEvent*evt);
virtual void wheelEvent(QWheelEvent* evt);
首先先说一下实现的摄像机。
#pragma once
#include "CELLMath.hpp"
namespace CELL
{
class CELLCamera
{
public:
float3 _eye;
float3 _up;
float3 _right;
float3 _target;
float3 _dir;
matrix4 _matView;
matrix4 _matProj;
matrix4 _matWorld;
float2 _viewSize;
public:
CELLCamera(const float3& target = float3(0,0,0),const float3& eye = float3(0,100,100),const float3& right = float3(1,0,0))
{
_viewSize = float2(256,256);
_matView = CELL::matrix4(1);
_matProj = CELL::matrix4(1);
_matWorld = CELL::matrix4(1);
_target = target;
_eye = eye;
_dir = normalize(_target - _eye);
_right = right;
_up = normalize(cross(_right,_dir));
}
~CELLCamera()
{}
float3 getEye() const
{
return _eye;
}
/**
* 设置眼睛的位置
*/
void setEye(CELL::float3 val)
{
_eye = val;
}
float3 getTarget() const
{
return _target;
}
void setTarget(CELL::float3 val)
{
_target = val;
}
void setRight(CELL::float3 val)
{
_right = val;
}
float3 getUp() const
{
return _up;
}
void setUp(CELL::float3 val)
{
_up = val;
}
float3 getDir() const
{
return _dir;
}
float3 getRight() const
{
return _right;
}
void setViewSize(const float2& viewSize)
{
_viewSize = viewSize;
}
void setViewSize(float x,float y)
{
_viewSize = float2(x,y);
}
float2 getViewSize()
{
return _viewSize;
}
void setProject(const matrix4& proj)
{
_matProj = proj;
}
const matrix4& getProject() const
{
return _matProj;
}
const matrix4& getView() const
{
return _matView;
}
/**
* 正交投影
*/
void ortho( float left, float right, float bottom, float top, float zNear, float zFar )
{
_matProj = CELL::ortho(left,right,bottom,top,zNear,zFar);
}
/**
* 透视投影
*/
void perspective(float fovy, float aspect, float zNear, float zFar)
{
_matProj = CELL::perspective<float>(fovy,aspect,zNear,zFar);
}
/**
* 世界坐标转化为窗口坐标
*/
bool project( const float4& world, float4& screen )
{
screen = (_matProj * _matView * _matWorld) * world;
if (screen.w == 0.0f)
{
return false;
}
screen.x /= screen.w;
screen.y /= screen.w;
screen.z /= screen.w;
// map to range 0 - 1
screen.x = screen.x * 0.5f + 0.5f;
screen.y = screen.y * 0.5f + 0.5f;
screen.z = screen.z * 0.5f + 0.5f;
// map to viewport
screen.x = screen.x * _viewSize.x;
screen.y = _viewSize.y - (screen.y * _viewSize.y);
return true;
}
/**
* 世界坐标转化为窗口坐标
*/
float2 worldToScreen( const float3& world)
{
float4 worlds(world.x,world.y,world.z,1);
float4 screens;
project(worlds,screens);
return float2(screens.x,screens.y);
}
/**
* 窗口坐标转化为世界坐标
*/
float3 screenToWorld(const float2& screen)
{
float4 screens(screen.x,screen.y,0,1);
float4 world;
unProject(screens,world);
return float3(world.x,world.y,world.z);
}
float3 screenToWorld(float x,float y)
{
float4 screens(x,y,0,1);
float4 world;
unProject(screens,world);
return float3(world.x,world.y,world.z);
}
/**
* 窗口坐标转化为世界坐标
*/
bool unProject( const float4& screen, float4& world )
{
float4 v;
v.x = screen.x;
v.y = screen.y;
v.z = screen.z;
v.w = 1.0;
// map from viewport to 0 - 1
v.x = (v.x) /_viewSize.x;
v.y = (_viewSize.y - v.y) /_viewSize.y;
//v.y = (v.y - _viewPort.Y) / _viewPort.Height;
// map to range -1 to 1
v.x = v.x * 2.0f - 1.0f;
v.y = v.y * 2.0f - 1.0f;
v.z = v.z * 2.0f - 1.0f;
CELL::matrix4 inverse = (_matProj * _matView * _matWorld).inverse();
v = v * inverse;
if (v.w == 0.0f)
{
return false;
}
world = v / v.w;
return true;
}
Ray createRayFromScreen(int x,int y)
{
float4 minWorld;
float4 maxWorld;
float4 screen(float(x),float(y),0,1);
float4 screen1(float(x),float(y),1,1);
unProject(screen,minWorld);
unProject(screen1,maxWorld);
Ray ray;
ray.setOrigin(float3(minWorld.x,minWorld.y,minWorld.z));
float3 dir(maxWorld.x - minWorld.x,maxWorld.y - minWorld.y, maxWorld.z - minWorld.z);
ray.setDirection(normalize(dir));
return ray;
}
/**
* 下面的函数的功能是将摄像机的观察方向绕某个方向轴旋转一定的角度
* 改变观察者的位置,目标的位置不变化
*/
virtual void rotateViewY(float angle)
{
_dir = rotateY<float>(_dir, angle);
_up = rotateY<float>(_up, angle);
_right = normalize(cross(_dir,_up));
float len = length(_eye - _target);
_eye = _target - _dir * len;
_matView = CELL::lookAt(_eye,_target,_up);
}
virtual void rotateViewX(float angle)
{
matrix4 mat(1) ;
mat.rotate(angle,_right);
_dir = _dir * mat;
_up = _up * mat;
_right = normalize(cross(_dir,_up));
float len = length(_eye - _target);
_eye = _target - _dir * len;
_matView = CELL::lookAt(_eye,_target,_up);
}
virtual void update()
{
_matView = CELL::lookAt(_eye,_target,_up);
}
};
}
这是摄像机的代码,其中定义了一些摄像机的基本信息,包括,眼睛的位置,视点位置,向上的向量,由这三个元素就可以确定一个摄像机的坐标系,里面还将投影矩阵进行维护。
下面说一下用户操作鼠标后模型是怎么进行变动的。首先是鼠标按下事件。
void OGLWidget::mousePressEvent( QMouseEvent*evt )
{
switch (evt->button())
{
case Qt::LeftButton:
{
_leftBtnFlag = true;
_mousePos = QPoint(evt->x(),evt->y());
if (_optionMode == 1)
{
m_pickObj = pickNode(evt->x(), evt->y());
}
}
break;
case Qt::RightButton:
{
_rightBtnFlag = true;
_mousePos = QPoint(evt->x(),evt->y());
}
break;
case Qt::MidButton:
break;
}
}
这里面分为鼠标左击和鼠标右击,鼠标左击主要是用于控制场景的移动,鼠标的右击用于控制场景的旋转,左击的_optionMode是用于控制是移动模型还是移动场景的标志位,由于需要在移动的时候知道是左键按下的还是右键按下的,所以需要有一个标志位来进行判断。
鼠标释放事件
void OGLWidget::mouseReleaseEvent( QMouseEvent*evt )
{
switch (evt->button())
{
case Qt::LeftButton:
{
_leftBtnFlag = false;
}
break;
case Qt::RightButton:
{
_rightBtnFlag = false;
}
break;
case Qt::MidButton:
break;
}
}
用于取消标志位,说明用户已经不进行操作场景。
鼠标移动事件
void OGLWidget::mouseMoveEvent( QMouseEvent*evt )
{
if (_leftBtnFlag)
{
if (_optionMode == 0)
{
CELL::Ray ray0 = _camera.createRayFromScreen(_mousePos.x(), _mousePos.y());
CELL::Ray ray1 = _camera.createRayFromScreen(evt->x(), evt->y());
CELL::float3 pos1 = calcIntersectPoint(ray0);
CELL::float3 pos2 = calcIntersectPoint(ray1);
_mousePos = QPoint(evt->x(), evt->y());
CELL::float3 offset = pos1 - pos2;
CELL::float3 newEye = _camera.getEye() + offset;
CELL::float3 newTgt = _camera.getTarget() + offset;
_camera.setEye(newEye);
_camera.setTarget(newTgt);
_camera.update();
}
else if (_optionMode == 1)
{
CELL::CELLNode* node = dynamic_cast<CELL::CELLNode*>(m_pickObj);
if (node)
{
CELL::Ray ray0 = _camera.createRayFromScreen(_mousePos.x(), _mousePos.y());
CELL::Ray ray1 = _camera.createRayFromScreen(evt->x(), evt->y());
CELL::float3 pos1 = calcIntersectPoint(ray0);
CELL::float3 pos2 = calcIntersectPoint(ray1);
_mousePos = QPoint(evt->x(), evt->y());
CELL::float3 offset = pos2 - pos1;
for (iterator itr = begin(); itr != end(); ++itr)
{
CELL::CELLNode* node = *itr;
if (node->hasFlag(CELL::CELLNode::FLAG_SELECT))
{
node->setTranslation(node->getTrans() + offset);
}
}
}
else
{
CELL::Ray ray0 = _camera.createRayFromScreen(_mousePos.x(), _mousePos.y());
CELL::Ray ray1 = _camera.createRayFromScreen(evt->x(), evt->y());
CELL::float3 pos1 = calcIntersectPoint(ray0);
CELL::float3 pos2 = calcIntersectPoint(ray1);
float xMin = CELL::tmin<float>(pos1.x, pos2.x);
float xMax = CELL::tmax<float>(pos1.x, pos2.x);
float zMin = CELL::tmin<float>(pos1.z, pos2.z);
float zMax = CELL::tmax<float>(pos1.z, pos2.z);
float yMin = 0;
float yMax = 9999999;
CELL::aabb3d aabbMouse;
aabbMouse.setExtents(xMin, yMin, zMin, xMax, yMax, zMax);
for (iterator itr = begin(); itr != end(); ++itr)
{
CELL::CELLNode* node = *itr;
CELL::aabb3d aabb = node->getAabb();
aabb.transform(node->getLocal());
if (aabbMouse.intersects(aabb))
{
node->setFlag(CELL::CELLNode::FLAG_SELECT);
}
else
{
node->removeFlag(CELL::CELLNode::FLAG_SELECT);
}
}
}
}
}
else if(_rightBtnFlag)
{
QPoint ptCur = QPoint(evt->x(),evt->y());
QPoint offset = ptCur - _mousePos;
_mousePos = ptCur;
_camera.rotateViewY(offset.x() * 0.3f);
_camera.rotateViewX(offset.y() * 0.3f);
}
}
先介绍一下鼠标右键移动的事件,主要是根据鼠标移动的位置,重新计算摄像机的摄像机矩阵,核心的思想就是保持观察者的位置不变,然后重新计算视点向量和向上的向量,再根据_eye = _target - _dir * len;计算出观察者的位置再重新计算摄像机矩阵后即可。
然后是鼠标左击事件,这里需要分为两部分来说,首先是移动场景的部分
if (_optionMode == 0)
{
CELL::Ray ray0 = _camera.createRayFromScreen(_mousePos.x(), _mousePos.y());
CELL::Ray ray1 = _camera.createRayFromScreen(evt->x(), evt->y());
CELL::float3 pos1 = calcIntersectPoint(ray0);
CELL::float3 pos2 = calcIntersectPoint(ray1);
_mousePos = QPoint(evt->x(), evt->y());
CELL::float3 offset = pos1 - pos2;
CELL::float3 newEye = _camera.getEye() + offset;
CELL::float3 newTgt = _camera.getTarget() + offset;
_camera.setEye(newEye);
_camera.setTarget(newTgt);
_camera.update();
}
这里是通过射线求交的方式计算出与模型的交点后,根据上一次的交点和这一次的交点计算出一个偏移量后,重新计算摄像机的视点位置和摄像机的位置更新到摄像机中即可。
还有一种是根据鼠标左键移动模型
else if (_optionMode == 1)
{
CELL::CELLNode* node = dynamic_cast<CELL::CELLNode*>(m_pickObj);
if (node)
{
CELL::Ray ray0 = _camera.createRayFromScreen(_mousePos.x(), _mousePos.y());
CELL::Ray ray1 = _camera.createRayFromScreen(evt->x(), evt->y());
CELL::float3 pos1 = calcIntersectPoint(ray0);
CELL::float3 pos2 = calcIntersectPoint(ray1);
_mousePos = QPoint(evt->x(), evt->y());
CELL::float3 offset = pos2 - pos1;
for (iterator itr = begin(); itr != end(); ++itr)
{
CELL::CELLNode* node = *itr;
if (node->hasFlag(CELL::CELLNode::FLAG_SELECT))
{
node->setTranslation(node->getTrans() + offset);
}
}
}
else
{
CELL::Ray ray0 = _camera.createRayFromScreen(_mousePos.x(), _mousePos.y());
CELL::Ray ray1 = _camera.createRayFromScreen(evt->x(), evt->y());
CELL::float3 pos1 = calcIntersectPoint(ray0);
CELL::float3 pos2 = calcIntersectPoint(ray1);
float xMin = CELL::tmin<float>(pos1.x, pos2.x);
float xMax = CELL::tmax<float>(pos1.x, pos2.x);
float zMin = CELL::tmin<float>(pos1.z, pos2.z);
float zMax = CELL::tmax<float>(pos1.z, pos2.z);
float yMin = 0;
float yMax = 9999999;
CELL::aabb3d aabbMouse;
aabbMouse.setExtents(xMin, yMin, zMin, xMax, yMax, zMax);
for (iterator itr = begin(); itr != end(); ++itr)
{
CELL::CELLNode* node = *itr;
CELL::aabb3d aabb = node->getAabb();
aabb.transform(node->getLocal());
if (aabbMouse.intersects(aabb))
{
node->setFlag(CELL::CELLNode::FLAG_SELECT);
}
else
{
node->removeFlag(CELL::CELLNode::FLAG_SELECT);
}
}
}
}
这里也分为两个情况,如果这次点击的位置没有拾取到模型的话,就说明用户想要绘制一个区域来选中模型(目前没有实现绘制区域的矩形),根据这两个点生成一个包围盒,根据这个包围盒遍历所有的节点进行求交,有交点的话就将模型的标志位增加选中状态,如果拾取到模型的话就遍历节点如果是有选中状态的话就移动模型。
改变窗口大小事件
void OGLWidget::resizeEvent( QResizeEvent *evt )
{
glViewport(0,0,width(),height());
/没有写这个导致的问题
_camera.setViewSize(width(),height());
_camera.perspective(60.0f,float(width())/float(height()),0.1f,10000.0f);
}
就是改变视口的大小和摄像机的投影矩阵就行。
联动模块
这个模块主要是要实现,当模型拖入场景后左侧下方的树需要增加模型的列表,还有当点击左侧下方的树后需要在右侧显示对应模型的属性。
void OGLWidget::dropEvent( QDropEvent *evt )
{
if (_frameWindow == nullptr)
{
return;
}
QString strFileName;
QByteArray data = evt->mimeData()->data("image/x-puzzle-piece");
QDataStream dataStream(&data, QIODevice::ReadOnly);
dataStream >> strFileName;
int iCnt = _frameWindow->getTreeWidget()->topLevelItemCount();
QTreeWidgetItem* pFind = NULL;
QTreeWidgetItem* pItem = NULL;
for (int i = 0; i < iCnt; ++i)
{
QTreeWidgetItem* pTop = _frameWindow->getTreeWidget()->topLevelItem(i);
QString text = pTop->text(0);
if (text == strFileName)
{
pFind = pTop;
break;
}
}
CELL::CELLNode* node = createNode(strFileName.toStdString().c_str(), evt->pos().x(), evt->pos().y());
if (node == NULL)
{
return;
}
if (pFind)
{
pItem = new QTreeWidgetItem(pFind);
pItem->setData(0, Qt::UserRole, QVariant((qlonglong)node));
pItem->setText(0, node->getName());
}
else
{
pFind = new QTreeWidgetItem(_frameWindow->getTreeWidget());
pFind->setText(0, strFileName);
pItem = new QTreeWidgetItem(pFind);
pItem->setData(0, Qt::UserRole, QVariant((qlonglong)node));
pItem->setText(0, node->getName());
}
evt->accept();
}
/// 模型列表槽函数处理过程
void GameDesigner::slotItemChanged(QListWidgetItem* item)
{
QString file = item->data(Qt::UserRole + 1).toString();
QtVariantProperty* prop = _pModelAttrMgr->addProperty(QVariant::String, "FileName");
prop->setValue(file);
ui._propertyTree->clear();
ui._propertyTree->addProperty(prop);
}
/// 场景树模型槽处理函数
void GameDesigner::slotitemClicked(QTreeWidgetItem* item, int)
{
CELL::CELLNode* pNode = (CELL::CELLNode*)item->data(0, Qt::UserRole).toLongLong();
if (pNode == NULL)
{
ui._propertyTree->clear();
return;
}
else
{
ui._renderWidget->resetSelect();
pNode->setFlag(CELL::CELLNode::FLAG_SELECT);
QtVariantProperty* trans = _pModelAttrMgr->addProperty(QVariant::String, "Pos");
QtVariantProperty* transX = _pModelAttrMgr->addProperty(QVariant::Double, "x");
QtVariantProperty* transY = _pModelAttrMgr->addProperty(QVariant::Double, "y");
QtVariantProperty* transZ = _pModelAttrMgr->addProperty(QVariant::Double, "z");
trans->addSubProperty(transX);
trans->addSubProperty(transY);
trans->addSubProperty(transZ);
transX->setValue(pNode->getTrans().x);
transY->setValue(pNode->getTrans().y);
transZ->setValue(pNode->getTrans().z);
transX->setUserData((void*)pNode);
transY->setUserData((void*)pNode);
transZ->setUserData((void*)pNode);
transX->setUserData1((void*)AttributeNodeType::ANT_POS_X);
transY->setUserData1((void*)AttributeNodeType::ANT_POS_Y);
transZ->setUserData1((void*)AttributeNodeType::ANT_POS_Z);
QtVariantProperty* scale = _pModelAttrMgr->addProperty(QVariant::String, "Scale");
QtVariantProperty* scaleX = _pModelAttrMgr->addProperty(QVariant::Double, "x");
QtVariantProperty* scaleY = _pModelAttrMgr->addProperty(QVariant::Double, "y");
QtVariantProperty* scaleZ = _pModelAttrMgr->addProperty(QVariant::Double, "z");
scale->addSubProperty(scaleX);
scale->addSubProperty(scaleY);
scale->addSubProperty(scaleZ);
scaleX->setValue(pNode->getScale().x);
scaleY->setValue(pNode->getScale().y);
scaleZ->setValue(pNode->getScale().z);
scaleX->setUserData((void*)pNode);
scaleY->setUserData((void*)pNode);
scaleZ->setUserData((void*)pNode);
scaleX->setUserData1((void*)AttributeNodeType::ANT_SCALE_X);
scaleY->setUserData1((void*)AttributeNodeType::ANT_SCALE_Y);
scaleZ->setUserData1((void*)AttributeNodeType::ANT_SCALE_Z);
ui._propertyTree->clear();
ui._propertyTree->addProperty(trans);
ui._propertyTree->addProperty(scale);
}
}
void GameDesigner::mySlotValueChanged(QtProperty* prop, const QVariant& val)
{
QtVariantProperty* propertys = dynamic_cast<QtVariantProperty*>(prop);
CELL::CELLNode* pNode = (CELL::CELLNode*)propertys->getUserData();
if (propertys == NULL || pNode == NULL)
{
return;
}
CELL::float3 trans = pNode->getTrans();
CELL::float3 scale = pNode->getScale();
switch ((int)propertys->getUserData1())
{
case AttributeNodeType::ANT_POS_X:
{
trans.x = val.toReal();
break;
}
case AttributeNodeType::ANT_POS_Y:
{
trans.y = val.toReal();
break;
}
case AttributeNodeType::ANT_POS_Z:
{
trans.z = val.toReal();
break;
}
case AttributeNodeType::ANT_SCALE_X:
{
scale.x = val.toReal();
break;
}
case AttributeNodeType::ANT_SCALE_Y:
{
scale.y = val.toReal();
break;
}
case AttributeNodeType::ANT_SCALE_Z:
{
scale.z = val.toReal();
break;
}
default:
break;
}
pNode->setTranslation(trans);
pNode->setScale(scale);
}
主要是这些函数,里面具体的逻辑可以看我分享的代码。
代码链接