Introduction
I have created a dynamic grid system to use in our Ogre-based world editor. Because Ogre is such a great engine and has such a great community, we decided to give this piece of code to the community. You can find the original forum post here.
Orthographic View
When using an orthographic camera (top, left, right, ...), the grid system has the following features:
- Grid is automatically updated when necessary (and only when necessary)
- Grid lines are automatically inserted / removed when the pixel spacing between the lines reaches a certain treshold
- Major grid lines (full alpha) and minor grid lines (alpha-faded according to their pixel spacing)
- Options: grid color, division (in how many lines a grid is divided when zooming in), render layer (behind or in front of all objects)
Perspective Cameras
When using a perspective camera, we use a simple 3D grid for now.
TODO
- Finish support for an arbitrary orthographic camera direction (what to do with axes lines, ...)
- Implement dynamic splitting and merging of grid cells in perspective view (based on what?)
- Implement mini-axes rendering (so you can easily see the camera rotation)
- Implement mini-scale rendering (like on a road map, to easily see unit sizes)
- Support for multiple grids?
- Add example code to move objects over one axis or a two-axes plane, so the object stays under the mouse cursor in screen space
To use this, you just create an instance of the class, providing it with an ogre scene manager and viewport. Then you just enable it (ViewportGrid::enable and such).
You can set some options:
- grid colour: the color of the grid lines. Minor grid lines are alpha-faded according to their pixel spacing. Zero coordinate grid lines automatically get the color of the orthogonal axis, if the camera is pointed along one of these (red for X, green for Y, blue for Z).
- Division level: this determines when major grid lines are drawn (defaults to 10). It also has an influence on when grid lines are split or merged (they are split into 'division' new lines when zooming in, and 'division' lines are merged when zooming out)
- Render layer: you can determine where the grid is drawn in orthographic view: behind or in front of all objects. Defaults to behind, and ignored in perspective view (there, the grid is an actual 3d grid on the XZ-plane)
- Perspective size: the size (width and height) of the grid in perspective mode. Defaults to 100 units.
- Scale rendering (not implemented yet): set to true if you want scaling info to be rendered on an overlay (like on maps, so you can see how many units one grid is)
- Mini axes rendering (not implemented yet): set to to true if you want mini-axes to be rendered in an overlay (so you can easily see the direction of your camera, even when the zero-gridlines aren't visible)
It just takes information about the camera attached to the viewport to calculate the grid lines. The actual movement of the camera is up to you. In orthographic view, we always start with the camera positioned 10000 units from the origin, use a fixed FOV, and change the near clipping plane to zoom in/out. Panning is done by moving the camera in its up/right plane.
Updates
- (2010-02-24) Changed the license to MIT (copied from OGRE)
Source Code
[+] ViewportGrid.h
/*
-----------------------------------------------------------------------------
This source file is supposed to be used with OGRE
(Object-oriented Graphics Rendering Engine)
For the latest info, see http://www.ogre3d.org/
Copyright (c) 2007 Jeroen Dierckx
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------------------------------------------------------------------------
*/
#ifndef OGRE_VIEWPORTGRID_H
#define OGRE_VIEWPORTGRID_H
// Includes
#include <Ogre/OgreRenderTargetlistener.h>
#include <Ogre/OgreColourValue.h>
#include <Ogre/OgreMath.h>
#include <Ogre/OgreVector3.h>
namespace Ogre
{
/** Use this class to render a dynamically adjusting grid inside an Ogre viewport.
@todo
Make the grid work with an arbitrary rotated orthogonal camera (e.g. for working in object space mode).
Or should the grid be rendered in object space too then?
*/
class ViewportGrid: public RenderTargetListener
{
public:
//! The render layer of the grid in orthogonal view
enum RenderLayer
{
RL_BEHIND, //!< Behind all objects
RL_INFRONT //!< In front of all objects
};
protected:
// Member variables
SceneManager *m_pSceneMgr;
Viewport *m_pViewport;
bool m_enabled;
RenderLayer m_layer;
Camera *m_pPrevCamera;
bool m_prevOrtho;
Vector3 m_prevCamPos;
Real m_prevNear;
Radian m_prevFOVy;
Real m_prevAspectRatio;
bool m_forceUpdate;
ManualObject *m_pGrid;
bool m_created;
SceneNode *m_pNode;
ColourValue m_colour1, m_colour2;
unsigned int m_division;
Real m_perspSize;
bool m_renderScale, m_renderMiniAxes;
public:
// Constructors and destructor
ViewportGrid(SceneManager *pSceneMgr, Viewport *pViewport);
virtual ~ViewportGrid();
//! Grid colour
const ColourValue &getColour() const { return m_colour1; }
void setColour(const ColourValue &colour);
//! Grid division (the number of new lines that are created when zooming in).
unsigned int getDivision() const { return m_division; }
void setDivision(unsigned int division);
//! Grid render layer (behind of in front of the objects).
RenderLayer getRenderLayer() const { return m_layer; }
void setRenderLayer(RenderLayer layer);
//! Size of the grid in perspective view
Real getPerspectiveSize() const { return m_perspSize; }
void setPerspectiveSize(Real size);
//! Render scaling info? Defaults to true.
//! @todo Implement this
bool getRenderScale() const { return m_renderScale; }
void setRenderScale(bool enabled = true);
//! Render mini axes? Defaults to true.
//! @todo Implement this
bool getRenderMiniAxes() const { return m_renderMiniAxes; }
void setRenderMiniAxes(bool enabled = true);
// Enable / disable
bool isEnabled() const;
void enable();
void disable();
void toggle();
void setEnabled(bool enabled);
// Other functions
void forceUpdate() { m_forceUpdate = true; }
protected:
// RenderTargetListener
void preViewportUpdate(const RenderTargetViewportEvent &evt);
void postViewportUpdate(const RenderTargetViewportEvent &evt);
// Other protected functions
void createGrid();
void destroyGrid();
void update();
void updateOrtho();
void updatePersp();
bool checkUpdate();
bool checkUpdateOrtho();
bool checkUpdatePersp();
};
}
#endif // OGRE_VIEWPORTGRID_H
[+] ViewportGrid.cpp
/*
-----------------------------------------------------------------------------
This source file is supposed to be used with OGRE
(Object-oriented Graphics Rendering Engine)
For the latest info, see http://www.ogre3d.org/
Copyright (c) 2007 Jeroen Dierckx
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-----------------------------------------------------------------------------
*/
// Includes
#include "ViewportGrid.h"
#include <Ogre/OgreManualObject.h>
#include <Ogre/OgreViewport.h>
#include <Ogre/OgreRenderTarget.h>
#include <Ogre/OgreSceneManager.h>
#include <Ogre/OgreStringConverter.h>
#include <Ogre/OgreMaterialManager.h>
using namespace Ogre;
// Constants
static const String sMatName = "ViewportGrid";
/******************************
* Constructors and destructor *
******************************/
ViewportGrid::ViewportGrid(SceneManager *pSceneMgr, Viewport *pViewport)
: m_pSceneMgr(pSceneMgr), m_pViewport(pViewport), m_enabled(false)
, m_pPrevCamera(0), m_prevOrtho(false), m_prevNear(0), m_prevFOVy(0), m_prevAspectRatio(0), m_forceUpdate(true)
, m_pGrid(0), m_created(false), m_pNode(0)
, m_colour1(0.7, 0.7, 0.7), m_colour2(0.7, 0.7, 0.7), m_division(10), m_perspSize(100)
, m_renderScale(true), m_renderMiniAxes(true)
{
assert(m_pSceneMgr);
assert(m_pViewport);
createGrid();
setRenderLayer(RL_BEHIND);
// Add this as a render target listener
m_pViewport->getTarget()->addListener(this);
}
ViewportGrid::~ViewportGrid()
{
// Remove this as a render target listener
m_pViewport->getTarget()->removeListener(this);
destroyGrid();
}
/************************
* Get and set functions *
************************/
/** Sets the colour of the major grid lines (the minor lines are alpha-faded out/in when zooming out/in)
@note The alpha value is automatically set to one
*/
void ViewportGrid::setColour(const ColourValue &colour)
{
// Force alpha = 1 for the primary colour
m_colour1 = colour; m_colour1.a = 1.0f;
m_colour2 = m_colour1;
forceUpdate();
}
/** Sets in how many lines a grid has to be divided when zoomed in.
Defaults to 10.
*/
void ViewportGrid::setDivision(unsigned int division)
{
m_division = division;
forceUpdate();
}
/** Sets the render layer of the grid
@note Ignored in perspective view.
@see Ogre::ViewportGrid::RenderLayer
*/
void ViewportGrid::setRenderLayer(RenderLayer layer)
{
m_layer = layer;
switch(m_layer)
{
default:
case RL_BEHIND:
// Render just before the world geometry
m_pGrid->setRenderQueueGroup(RENDER_QUEUE_WORLD_GEOMETRY_1 - 1);
break;
case RL_INFRONT:
// Render just before the overlays
m_pGrid->setRenderQueueGroup(RENDER_QUEUE_OVERLAY - 1);
break;
}
}
/** Sets the size of the grid in perspective view.
Defaults to 100 units.
@note Ignored in orthographic view.
*/
void ViewportGrid::setPerspectiveSize(Real size)
{
m_perspSize = size;
forceUpdate();
}
/** Sets whether to render scaling info in an overlay.
This looks a bit like the typical scaling info on a map.
*/
void ViewportGrid::setRenderScale(bool enabled)
{
m_renderScale = enabled;
forceUpdate();
}
/** Sets whether to render mini-axes in an overlay.
*/
void ViewportGrid::setRenderMiniAxes(bool enabled)
{
m_renderMiniAxes = enabled;
forceUpdate();
}
/******************
* Other functions *
******************/
bool ViewportGrid::isEnabled() const
{
return m_enabled;
}
void ViewportGrid::enable()
{
m_enabled = true;
if(!m_pGrid->isAttached())
m_pNode->attachObject(m_pGrid);
forceUpdate();
}
void ViewportGrid::disable()
{
m_enabled = false;
if(m_pGrid->isAttached())
m_pNode->detachObject(m_pGrid);
}
void ViewportGrid::toggle()
{
setEnabled(!m_enabled);
}
void ViewportGrid::setEnabled(bool enabled)
{
if(enabled)
enable();
else
disable();
}
/***********************
* RenderTargetListener *
***********************/
void ViewportGrid::preViewportUpdate(const RenderTargetViewportEvent &evt)
{
if(evt.source != m_pViewport) return;
m_pGrid->setVisible(true);
if(m_enabled)
update();
}
void ViewportGrid::postViewportUpdate(const RenderTargetViewportEvent &evt)
{
if(evt.source != m_pViewport) return;
m_pGrid->setVisible(false);
}
/****************************
* Other protected functions *
****************************/
void ViewportGrid::createGrid()
{
String name = m_pViewport->getTarget()->getName() + "::";
name += StringConverter::toString(m_pViewport->getZOrder()) + "::ViewportGrid";
// Create the manual object
m_pGrid = m_pSceneMgr->createManualObject(name);
m_pGrid->setDynamic(true);
// Create the scene node (not attached yet)
m_pNode = m_pSceneMgr->getRootSceneNode()->createChildSceneNode(name);
m_enabled = false;
// Make sure the material is created
//! @todo Should we destroy the material somewhere?
MaterialManager &matMgr = MaterialManager::getSingleton();
if(!matMgr.resourceExists(sMatName))
{
MaterialPtr pMaterial = matMgr.create(sMatName, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
pMaterial->setLightingEnabled(false);
pMaterial->setSceneBlending(SBT_TRANSPARENT_ALPHA);
}
}
void ViewportGrid::destroyGrid()
{
// Destroy the manual object
m_pSceneMgr->destroyManualObject(m_pGrid);
m_pGrid = 0;
// Destroy the scene node
m_pSceneMgr->destroySceneNode(m_pNode->getName());
m_pNode = 0;
}
void ViewportGrid::update()
{
if(!m_enabled) return;
Camera *pCamera = m_pViewport->getCamera();
if(!pCamera) return;
// Check if an update is necessary
if(!checkUpdate() && !m_forceUpdate)
return;
if(pCamera->getProjectionType() == PT_ORTHOGRAPHIC)
updateOrtho();
else
updatePersp();
m_forceUpdate = false;
}
void ViewportGrid::updateOrtho()
{
// Screen dimensions
int width = m_pViewport->getActualWidth();
int height = m_pViewport->getActualHeight();
// Camera information
Camera *pCamera = m_pViewport->getCamera();
const Vector3 &camPos = pCamera->getPosition();
Vector3 camDir = pCamera->getDirection();
Vector3 camUp = pCamera->getUp();
Vector3 camRight = pCamera->getRight();
// Translation in grid space
Real dx = camPos.dotProduct(camRight);
Real dy = camPos.dotProduct(camUp);
// Frustum dimensions
// Note: Tan calculates the opposite side of a _right_ triangle given its angle, so we make sure it is one, and double the result
Real worldWidth = 2 * Math::Tan(pCamera->getFOVy() / 2) * pCamera->getAspectRatio() * pCamera->getNearClipDistance();
Real worldHeight = worldWidth / pCamera->getAspectRatio();
Real worldLeft = dx - worldWidth / 2;
Real worldRight = dx + worldWidth / 2;
Real worldBottom = dy - worldHeight / 2;
Real worldTop = dy + worldHeight / 2;
// Conversion values (note: same as working with the height values)
Real worldToScreen = width / worldWidth;
Real screenToWorld = worldWidth / width;
//! @todo Treshold should be dependent on window width/height (min? max?) so there are no more then m_division full alpha-lines
static const int treshold = 10; // Treshhold in pixels
// Calculate the spacing multiplier
Real mult = 0;
int exp = 0;
Real temp = worldToScreen; // 1 world unit
if(worldToScreen < treshold)
{
while(temp < treshold)
{
++exp;
temp *= treshold;
}
mult = Math::Pow(m_division, exp);
}
else
{
while(temp > m_division * treshold)
{
++exp;
temp /= treshold;
}
mult = Math::Pow(1.0f / m_division, exp);
}
// Interpolate alpha for (multiplied) spacing between treshold and m_division * treshold
m_colour2.a = worldToScreen * mult / (m_division * treshold - treshold);
if(m_colour2.a > 1.0f) m_colour2.a = 1.0f;
// Calculate the horizontal zero-axis color
Real camRightX = Math::Abs(camRight.x);
Real camRightY = Math::Abs(camRight.y);
Real camRightZ = Math::Abs(camRight.z);
const ColourValue &horAxisColor = Math::RealEqual(camRightX, 1.0f) ? ColourValue::Red
: Math::RealEqual(camRightY, 1.0f) ? ColourValue::Green
: Math::RealEqual(camRightZ, 1.0f) ? ColourValue::Blue : m_colour1;
// Calculate the vertical zero-axis color
Real camUpX = Math::Abs(camUp.x);
Real camUpY = Math::Abs(camUp.y);
Real camUpZ = Math::Abs(camUp.z);
const ColourValue &vertAxisColor = Math::RealEqual(camUpX, 1.0f) ? ColourValue::Red
: Math::RealEqual(camUpY, 1.0f) ? ColourValue::Green
: Math::RealEqual(camUpZ, 1.0f) ? ColourValue::Blue : m_colour1;
// The number of lines
int numLinesWidth = (int) (worldWidth / mult) + 1;
int numLinesHeight = (int) (worldHeight / mult) + 1;
// Start creating or updating the grid
m_pGrid->estimateVertexCount(2 * numLinesWidth + 2 * numLinesHeight);
if(m_created)
m_pGrid->beginUpdate(0);
else
{
m_pGrid->begin(sMatName, Ogre::RenderOperation::OT_LINE_LIST);
m_created = true;
}
// Vertical lines
Real startX = mult * (int) (worldLeft / mult);
Real x = startX;
while(x <= worldRight)
{
// Get the right color for this line
int multX = (x == 0) ? x : (x < 0) ? (int) (x / mult - 0.5f) : (int) (x / mult + 0.5f);
const ColourValue &colour = (multX == 0) ? vertAxisColor : (multX % (int) m_division) ? m_colour2 : m_colour1;
// Add the line
m_pGrid->position(x, worldBottom, 0);
m_pGrid->colour(colour);
m_pGrid->position(x, worldTop, 0);
m_pGrid->colour(colour);
x += mult;
}
// Horizontal lines
Real startY = mult * (int) (worldBottom / mult);
Real y = startY;
while(y <= worldTop)
{
// Get the right color for this line
int multY = (y == 0) ? y : (y < 0) ? (int) (y / mult - 0.5f) : (int) (y / mult + 0.5f);
const ColourValue &colour = (multY == 0) ? horAxisColor : (multY % (int) m_division) ? m_colour2 : m_colour1;
// Add the line
m_pGrid->position(worldLeft, y, 0);
m_pGrid->colour(colour);
m_pGrid->position(worldRight, y, 0);
m_pGrid->colour(colour);
y += mult;
}
m_pGrid->end();
m_pNode->setOrientation(pCamera->getOrientation());
}
void ViewportGrid::updatePersp()
{
//! @todo Calculate the spacing multiplier
Real mult = 1;
//! @todo Interpolate alpha
m_colour2.a = 0.5f;
//if(m_colour2.a > 1.0f) m_colour2.a = 1.0f;
// Calculate the horizontal zero-axis color
const ColourValue &horAxisColor = ColourValue::Red;
// Calculate the vertical zero-axis color
const ColourValue &vertAxisColor = ColourValue::Blue;
// The number of lines
int numLines = (int) (m_perspSize / mult) + 1;
// Start creating or updating the grid
m_pGrid->estimateVertexCount(4 * numLines);
if(m_created)
m_pGrid->beginUpdate(0);
else
{
m_pGrid->begin(sMatName, Ogre::RenderOperation::OT_LINE_LIST);
m_created = true;
}
// Vertical lines
Real start = mult * (int) (-m_perspSize / 2 / mult);
Real x = start;
while(x <= m_perspSize / 2)
{
// Get the right color for this line
int multX = (x == 0) ? x : (x < 0) ? (int) (x / mult - 0.5f) : (int) (x / mult + 0.5f);
const ColourValue &colour = (multX == 0) ? vertAxisColor : (multX % (int) m_division) ? m_colour2 : m_colour1;
// Add the line
m_pGrid->position(x, 0, -m_perspSize / 2);
m_pGrid->colour(colour);
m_pGrid->position(x, 0, m_perspSize / 2);
m_pGrid->colour(colour);
x += mult;
}
// Horizontal lines
Real y = start;
while(y <= m_perspSize / 2)
{
// Get the right color for this line
int multY = (y == 0) ? y : (y < 0) ? (int) (y / mult - 0.5f) : (int) (y / mult + 0.5f);
const ColourValue &colour = (multY == 0) ? horAxisColor : (multY % (int) m_division) ? m_colour2 : m_colour1;
// Add the line
m_pGrid->position(-m_perspSize / 2, 0, y);
m_pGrid->colour(colour);
m_pGrid->position(m_perspSize / 2, 0, y);
m_pGrid->colour(colour);
y += mult;
}
m_pGrid->end();
// Normal orientation, grid in the X-Z plane
m_pNode->resetOrientation();
}
/* Checks if an update is necessary */
bool ViewportGrid::checkUpdate()
{
bool update = false;
Camera *pCamera = m_pViewport->getCamera();
if(!pCamera) return false;
if(pCamera != m_pPrevCamera)
{
m_pPrevCamera = pCamera;
update = true;
}
bool ortho = (pCamera->getProjectionType() == PT_ORTHOGRAPHIC);
if(ortho != m_prevOrtho)
{
m_prevOrtho = ortho;
update = true;
// Set correct material properties
MaterialPtr pMaterial = MaterialManager::getSingleton().getByName(sMatName);
if(!pMaterial.isNull())
{
pMaterial->setDepthWriteEnabled(!ortho);
pMaterial->setDepthCheckEnabled(!ortho);
}
}
return update || ortho ? checkUpdateOrtho() : checkUpdatePersp();
}
bool ViewportGrid::checkUpdateOrtho()
{
bool update = false;
Camera *pCamera = m_pViewport->getCamera();
if(!pCamera) return false;
if(pCamera->getPosition() != m_prevCamPos)
{
m_prevCamPos = pCamera->getPosition();
update = true;
}
if(pCamera->getNearClipDistance() != m_prevNear)
{
m_prevNear = pCamera->getNearClipDistance();
update = true;
}
if(pCamera->getFOVy() != m_prevFOVy)
{
m_prevFOVy = pCamera->getFOVy();
update = true;
}
if(pCamera->getAspectRatio() != m_prevAspectRatio)
{
m_prevAspectRatio = pCamera->getAspectRatio();
update = true;
}
return update;
}
bool ViewportGrid::checkUpdatePersp()
{
//! @todo Add a check if grid line splitting/merging is implemented
return false;
}