了解红蓝眼镜原理之后剩下的事情就简单了
如果不清楚红蓝眼镜原理,请先看上一篇:制作立体图像(一):红蓝眼镜原理
另外你应该已经准备好了一副红蓝眼镜(如果没有请点击这里,然后关闭本页面:)
现在戴上眼镜,先看看我们要做到的最终效果,一个旋转的立体地球:
(当然这个是静止截图)
先说说实现原理:
- 在坐标原点创建一个圆球模型,并贴上地球纹理
- 在恰当位置创建两个相机,并将两个相机的结果渲染到左右纹理
- 绘制全屏四边形,并应用立体材质,材质中通过shader对步骤2的纹理做红绿蓝混合,这个全屏四边形就是我们最终想要的结果
以下是详细说明:
- 创建三维模型
这一步最重要的是制作一副高清的地球纹理图,类似下面这样
不过图片nasa早就为你准备好了,你可以到这里下载任何你想要的(鬼子真的很强大)
创建地球mesh的代码也早有人帮你写好了,详见附带文件中函数://根据mesh名称、半径、经纬线条数创建对应的mesh void MyApplication::createSphere(const std::string& meshName, const float r, const int nRings, const int nSegments)
- 相机设置
渲染到纹理,左眼使用主相机mCamera,需另创建右眼相机
//左眼纹理 Ogre::TexturePtr textureLeft = Ogre::TextureManager::getSingleton().createManual("textureLeft", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mWindow->getWidth(), mWindow->getHeight(), 0, Ogre::PF_R8G8B8, Ogre::TU_RENDERTARGET); Ogre::RenderTexture *targetLeft = textureLeft->getBuffer()->getRenderTarget(); targetLeft->addViewport(mCamera); //右眼纹理 Ogre::Camera *cameraRight = mSceneMgr->createCamera("cameraRight"); ...同上... ;
设置相机
//设置相机位置、焦距 const int x = 10, y = 150, z = 400; mCamera->setPosition(-x, y, z); cameraRight->setPosition(x, y, z); mCamera->lookAt(0, 0, 0); cameraRight->lookAt(0, 0, 0); mCamera->setFrustumOffset(x + x); mCamera->setFocalLength(Ogre::Math::Sqrt(x*x + y*y + z*z));
你可以通过设置很远的焦距和很小的fovy制作出看上去很远很大的地球 - 全屏四边形,最终的渲染效果
这里使用Ogre::Rectangle2D:
mScreen = new Ogre::Rectangle2D(true); mScreen->setCorners(-1, 1, 1, -1, true); mScreen->setMaterial("stereo/fp");
材质stereo/fp定义:(这里使用cg脚本以支持direct3d+opengl,同时代码也简短)
fragment_program fpCG cg { source stereo.cg entry_point RedCyan profiles ps_2_0 arbfp1 } material stereo/fpCG { technique { pass { fragment_program_ref fpCG{} texture_unit { texture textureLeft } texture_unit { texture textureRight } } } }
材质脚本指定了左右相机渲染的textureLeft、textureRight两幅纹理,并引用RedCyan着色器
CG脚本,stereo.cg:void RedCyan( float2 uv : TEXCOORD0, out float4 color :COLOR, uniform sampler2D t1 : register(s0), uniform sampler2D t2 : register(s1)) { color = float4(tex2D(t1, uv) * float4(1, 0, 0, 0) + tex2D(t2, uv) * float4(0, 1, 1, 1)); }
简单的取左右纹理对应红+绿蓝分量即可
注:这里用的乘法后相加,如果直接先取左右纹理颜色,再提取rgb分量的形式,如:color = float4(c1.r, c2.g, c2.b, 1)会导致与direct3d不兼容,and i don't konw why:( - 其它
因为我们使用全屏四边形,在左右相机渲染纹理的时候需要隐藏,不然有可能将我们的四边形渲染到纹理中
这里需要实现RenderTargerListener接口,在渲染前后做显隐控制:
virtual void preRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { mScreen->setVisible(false); } virtual void postRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { mScreen->setVisible(true); }
同时在createScene中注册对应的listener:
targetLeft->addListener(this); targetRight->addListener(this);
- 最后是锦上添花的一步:让我们的地球转起来
bool MyApplication::frameRenderingQueued(const Ogre::FrameEvent &evt) { mEarthNode->yaw(Ogre::Radian(evt.timeSinceLastFrame * 0.5)); return true; }
#pragma once #include <vector> #include <fstream> #include <string> #include <Ogre/Ogre.h> #include <OIS/OIS.h> class MyApplication: public Ogre::RenderTargetListener, public Ogre::FrameListener, public OIS::KeyListener { public: MyApplication(void){ mSceneMgr = NULL; mRoot = NULL; } ~MyApplication(void){ mInputManager->destroyInputObject(mKeyboard); mInputManager->destroyInputObject(mMouse); OIS::InputManager::destroyInputSystem(mInputManager); delete mRoot; } int startup(); private: void createScene(); virtual void preRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { mScreen->setVisible(false); } virtual void postRenderTargetUpdate(const Ogre::RenderTargetEvent& evt) { mScreen->setVisible(true); } Ogre::MovableObject* createSphere(); void createSphere(const std::string& meshName, const float r, const int nRings = 16, const int nSegments = 16); bool frameStarted(const Ogre::FrameEvent& evt); bool frameEnded(const Ogre::FrameEvent& evt); bool frameRenderingQueued(const Ogre::FrameEvent &evt); bool keyPressed(const OIS::KeyEvent &e); bool keyReleased(const OIS::KeyEvent &e) { return true; } void _createAxis(const int lenth); //创建坐标轴: x red, y green, z blue void _loadResources(const char* resoureFile); void _createInput(); void _showDebugOverlay(bool show); void _updateStats(void); void _keyPressedDefault(const OIS::KeyEvent &e); //默认键盘、鼠标导航 bool _navigateDefault(const Ogre::FrameEvent& evt); Ogre::SceneManager* mSceneMgr; Ogre::RenderWindow* mWindow; Ogre::Camera* mCamera; Ogre::Root* mRoot; Ogre::SceneNode* mRootNode; //根节点 OIS::InputManager* mInputManager; OIS::Keyboard* mKeyboard; OIS::Mouse* mMouse; Ogre::SceneNode* mEarthNode; Ogre::Rectangle2D* mScreen; int mNumScreenShots; //截图顺序号 bool mStatsOn; Ogre::Overlay* mDebugOverlay; };
//易变更部分 #include "MyApplication.h" void MyApplication::createScene() { mEarthNode = mRootNode->createChildSceneNode(); mEarthNode->attachObject(createSphere()); //左眼纹理 Ogre::TexturePtr textureLeft = Ogre::TextureManager::getSingleton().createManual("textureLeft", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mWindow->getWidth(), mWindow->getHeight(), 0, Ogre::PF_R8G8B8, Ogre::TU_RENDERTARGET); Ogre::RenderTexture *targetLeft = textureLeft->getBuffer()->getRenderTarget(); targetLeft->addViewport(mCamera); //右眼纹理 Ogre::Camera *cameraRight = mSceneMgr->createCamera("cameraRight"); cameraRight->setAspectRatio(Ogre::Real(mWindow->getWidth()) / Ogre::Real(mWindow->getHeight())); Ogre::TexturePtr textureRight = Ogre::TextureManager::getSingleton().createManual("textureRight", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, Ogre::TEX_TYPE_2D, mWindow->getWidth(), mWindow->getHeight(), 0, Ogre::PF_R8G8B8, Ogre::TU_RENDERTARGET); Ogre::RenderTexture *targetRight = textureRight->getBuffer()->getRenderTarget(); targetRight->addViewport(cameraRight); //设置相机位置、焦距 const int x = 10, y = 150, z = 400; mCamera->setPosition(-x, y, z); cameraRight->setPosition(x, y, z); mCamera->lookAt(0, 0, 0); cameraRight->lookAt(0, 0, 0); mCamera->setFrustumOffset(x + x); mCamera->setFocalLength(Ogre::Math::Sqrt(x*x + y*y + z*z)); mScreen = new Ogre::Rectangle2D(true); mScreen->setCorners(-1, 1, 1, -1, true); mScreen->setMaterial("stereo/fpCG"); mRootNode->attachObject(mScreen); targetLeft->addListener(this); targetRight->addListener(this); } bool MyApplication::keyPressed(const OIS::KeyEvent &e) { _keyPressedDefault(e); return true; } bool MyApplication::frameStarted(const Ogre::FrameEvent& evt) { //if(!_navigateDefault(evt)) return false; mKeyboard->capture(); if(mKeyboard->isKeyDown(OIS::KC_ESCAPE)){ return false; } return true; } bool MyApplication::frameEnded(const Ogre::FrameEvent& evt){ _updateStats(); return true; } bool MyApplication::frameRenderingQueued(const Ogre::FrameEvent &evt) { mEarthNode->yaw(Ogre::Radian(evt.timeSinceLastFrame * 0.5)); return true; } Ogre::MovableObject* MyApplication::createSphere(){ createSphere("mySphereMesh", 100, 100, 100); Ogre::Entity* sphereEntity = mSceneMgr->createEntity ("mySphereEntity", "mySphereMesh"); sphereEntity->setMaterialName("Test/earth"); return sphereEntity; } //根据mesh名称、半径、经纬线条数创建对应的mesh void MyApplication::createSphere(const std::string& meshName, const float r, const int nRings, const int nSegments) { Ogre::MeshPtr pSphere = Ogre::MeshManager::getSingleton().createManual(meshName, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); Ogre::SubMesh *pSphereVertex = pSphere->createSubMesh(); Ogre::VertexData* vertexData = new Ogre::VertexData(); pSphere->sharedVertexData = vertexData; // define the vertex format Ogre::VertexDeclaration* vertexDecl = vertexData->vertexDeclaration; size_t currOffset = 0; // positions vertexDecl->addElement(0, currOffset, Ogre::VET_FLOAT3, Ogre::VES_POSITION); currOffset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3); //// DIFFUSE //vertexDecl->addElement(0, currOffset, VET_FLOAT3, Ogre::VES_DIFFUSE); //currOffset += VertexElement::getTypeSize(VET_FLOAT3); // normals vertexDecl->addElement(0, currOffset, Ogre::VET_FLOAT3, Ogre::VES_NORMAL); currOffset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3); //// two dimensional texture coordinates vertexDecl->addElement(0, currOffset, Ogre::VET_FLOAT2, Ogre::VES_TEXTURE_COORDINATES, 0); currOffset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT2); // allocate the vertex buffer vertexData->vertexCount = (nRings + 1) * (nSegments+1); Ogre::HardwareVertexBufferSharedPtr vBuf = Ogre::HardwareBufferManager::getSingleton().createVertexBuffer(vertexDecl->getVertexSize(0), vertexData->vertexCount, Ogre::HardwareBuffer::HBU_STATIC_WRITE_ONLY, false); Ogre::VertexBufferBinding* binding = vertexData->vertexBufferBinding; binding->setBinding(0, vBuf); float* pVertex = static_cast<float*>(vBuf->lock(Ogre::HardwareBuffer::HBL_DISCARD)); // allocate index buffer pSphereVertex->indexData->indexCount = 6 * nRings * (nSegments + 1); pSphereVertex->indexData->indexBuffer = Ogre::HardwareBufferManager::getSingleton().createIndexBuffer(Ogre::HardwareIndexBuffer::IT_16BIT, pSphereVertex->indexData->indexCount, Ogre::HardwareBuffer::HBU_STATIC_WRITE_ONLY, false); Ogre::HardwareIndexBufferSharedPtr iBuf = pSphereVertex->indexData->indexBuffer; unsigned short* pIndices = static_cast<unsigned short*>(iBuf->lock(Ogre::HardwareBuffer::HBL_DISCARD)); float fDeltaRingAngle = float(Ogre::Math::PI / nRings); float fDeltaSegAngle = float(2 * Ogre::Math::PI / nSegments); unsigned short wVerticeIndex = 0 ; // Generate the group of rings for the sphere for( int ring = 0; ring <= nRings; ring++ ) { float r0 = r * sinf (ring * fDeltaRingAngle); float y0 = r * cosf (ring * fDeltaRingAngle); // Generate the group of segments for the current ring for(int seg = 0; seg <= nSegments; seg++) { float x0 = r0 * sinf(seg * fDeltaSegAngle); float z0 = r0 * cosf(seg * fDeltaSegAngle); // Add one vertex to the strip which makes up the sphere *pVertex++ = x0; *pVertex++ = y0; *pVertex++ = z0; Ogre::Vector3 vNormal = Ogre::Vector3(x0, y0, z0).normalisedCopy(); *pVertex++ = vNormal.x; *pVertex++ = vNormal.y; *pVertex++ = vNormal.z; *pVertex++ = (float) seg / (float) nSegments; *pVertex++ = (float) ring / (float) nRings; if (ring != nRings) { // each vertex (except the last) has six indices pointing to it *pIndices++ = wVerticeIndex + nSegments + 1; *pIndices++ = wVerticeIndex; *pIndices++ = wVerticeIndex + nSegments; *pIndices++ = wVerticeIndex + nSegments + 1; *pIndices++ = wVerticeIndex + 1; *pIndices++ = wVerticeIndex; wVerticeIndex ++; } }; // end for seg } // end for ring // Unlock vBuf->unlock(); iBuf->unlock(); // Generate face list pSphereVertex->useSharedVertices = true; // the original code was missing this line: pSphere->_setBounds( Ogre::AxisAlignedBox(Ogre::Vector3(-r, -r, -r), Ogre::Vector3(r, r, r) ), false ); pSphere->_setBoundingSphereRadius(r); // this line makes clear the mesh is loaded (avoids memory leaks) pSphere->load(); }
//系统不常变更部分实现 #include "MyApplication.h" #include "windows.h" int main(int argc, char *argv[]) { //设置当前工作目录,用于文件关联打开方式 std::string file(argv[0]); SetCurrentDirectoryA(file.substr(0, file.find_last_of("\\")).c_str()); MyApplication app; app.startup(); } int MyApplication::startup() { #ifdef _DEBUG mRoot = new Ogre::Root("../plugins_d.cfg", "../ogre.cfg", "../Ogre.log"); #else mRoot = new Ogre::Root("../plugins.cfg", "../ogre.cfg", "../Ogre.log"); #endif if(!mRoot->showConfigDialog()){ //if(!mRoot->showConfigDialog()){ return -1; } mWindow = mRoot->initialise(true, "Ogre3D"); mSceneMgr = mRoot->createSceneManager(Ogre::ST_EXTERIOR_CLOSE); mCamera = mSceneMgr->createCamera("camera"); mCamera->setPosition(Ogre::Vector3(100, 200, 300)); mCamera->lookAt(Ogre::Vector3(0, 0, 0)); mCamera->setNearClipDistance(10); //default [100, 100 * 1000] Ogre::Viewport* viewport = mWindow->addViewport(mCamera); viewport->setBackgroundColour(Ogre::ColourValue(0.0, 0.0, 0.0)); mCamera->setAspectRatio(Ogre::Real(viewport->getActualWidth())/Ogre::Real(viewport->getActualHeight())); mRootNode = mSceneMgr->getRootSceneNode(); _loadResources("../resources_testStereo.cfg"); createScene(); _createAxis(100); _createInput(); mDebugOverlay = Ogre::OverlayManager::getSingleton().getByName("Core/DebugOverlay"); _showDebugOverlay(true); mRoot->addFrameListener(this); mRoot->startRendering(); return 0; } void MyApplication::_createAxis(const int lenth) { Ogre::ManualObject *mo = mSceneMgr->createManualObject(); mo->begin("BaseWhiteNoLighting", Ogre::RenderOperation::OT_LINE_LIST); mo->position(lenth, 0, 0); mo->colour(1.0, 0, 0); mo->position(0, 0, 0); mo->colour(1.0, 0, 0); mo->position(0, lenth, 0); mo->colour(0, 1.0, 0); mo->position(0, 0, 0); mo->colour(0, 1.0, 0); mo->position(0, 0, lenth); mo->colour(0, 0, 1.0); mo->position(0 , 0, 0); mo->colour(0, 0, 1.0); mo->end(); mRootNode->attachObject(mo); } void MyApplication::_loadResources(const char* resourceFile) { Ogre::ConfigFile cf; cf.load(resourceFile); Ogre::ConfigFile::SectionIterator sectionIter = cf.getSectionIterator(); Ogre::String sectionName, typeName, dataName; while(sectionIter.hasMoreElements()){ sectionName = sectionIter.peekNextKey(); Ogre::ConfigFile::SettingsMultiMap *settings = sectionIter.getNext(); Ogre::ConfigFile::SettingsMultiMap::iterator i; for(i=settings->begin(); i!=settings->end(); i++){ typeName =i->first; dataName = i->second; Ogre::ResourceGroupManager::getSingleton().addResourceLocation(dataName, typeName, sectionName); } } Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups(); } void MyApplication::_updateStats(void) { static Ogre::String currFps = "Current FPS: "; static Ogre::String avgFps = "Average FPS: "; static Ogre::String bestFps = "Best FPS: "; static Ogre::String worstFps = "Worst FPS: "; static Ogre::String tris = "Triangle Count: "; static Ogre::String batches = "Batch Count: "; // update stats when necessary try { Ogre::OverlayElement* guiAvg = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/AverageFps"); Ogre::OverlayElement* guiCurr = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/CurrFps"); Ogre::OverlayElement* guiBest = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/BestFps"); Ogre::OverlayElement* guiWorst = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/WorstFps"); const Ogre::RenderTarget::FrameStats& stats = mWindow->getStatistics(); guiAvg->setCaption(avgFps + Ogre::StringConverter::toString(stats.avgFPS)); guiCurr->setCaption(currFps + Ogre::StringConverter::toString(stats.lastFPS)); guiBest->setCaption(bestFps + Ogre::StringConverter::toString(stats.bestFPS) +" "+Ogre::StringConverter::toString(stats.bestFrameTime)+" ms"); guiWorst->setCaption(worstFps + Ogre::StringConverter::toString(stats.worstFPS) +" "+Ogre::StringConverter::toString(stats.worstFrameTime)+" ms"); Ogre::OverlayElement* guiTris = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/NumTris"); guiTris->setCaption(tris + Ogre::StringConverter::toString(std::max((int)stats.triangleCount, 230) - 230)); Ogre::OverlayElement* guiBatches = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/NumBatches"); guiBatches->setCaption(batches + Ogre::StringConverter::toString((int)stats.batchCount - 10)); //Ogre::OverlayElement* guiDbg = Ogre::OverlayManager::getSingleton().getOverlayElement("Core/DebugText"); //guiDbg->setCaption("mDebugText"); } catch(...) { /* ignore */ } } void MyApplication::_showDebugOverlay(bool show) { if (mDebugOverlay) { if (show) mDebugOverlay->show(); else mDebugOverlay->hide(); } } void MyApplication::_createInput() { OIS::ParamList parameters; unsigned int windowHandle = 0; std::ostringstream windowHandleString; mWindow->getCustomAttribute("WINDOW", &windowHandle); windowHandleString<<windowHandle; parameters.insert(std::make_pair("WINDOW", windowHandleString.str())); mInputManager = OIS::InputManager::createInputSystem(parameters); mKeyboard = static_cast<OIS::Keyboard*>(mInputManager->createInputObject(OIS::OISKeyboard, true)); mMouse = static_cast<OIS::Mouse*>(mInputManager->createInputObject(OIS::OISMouse, true)); mKeyboard->setEventCallback(this); } void MyApplication::_keyPressedDefault(const OIS::KeyEvent &e) { if(e.key == OIS::KC_SYSRQ) { std::ostringstream ss; ss << "screenshot_" << ++mNumScreenShots << ".png"; mWindow->writeContentsToFile(ss.str()); } else if(e.key == OIS::KC_G) { mStatsOn = !mStatsOn; _showDebugOverlay(mStatsOn); } else if(e.key == OIS::KC_R) { if(mCamera->getPolygonMode() == Ogre::PM_SOLID) { mCamera->setPolygonMode(Ogre::PM_WIREFRAME); } else { mCamera->setPolygonMode(Ogre::PM_SOLID); } } } //默认键盘、鼠标导航 bool MyApplication::_navigateDefault(const Ogre::FrameEvent& evt) { mKeyboard->capture(); if(mKeyboard->isKeyDown(OIS::KC_ESCAPE)){ return false; } Ogre::Vector3 translate(0, 0, 0); if(mKeyboard->isKeyDown(OIS::KC_W)){ translate +=Ogre::Vector3(0, 0, -1); } if(mKeyboard->isKeyDown(OIS::KC_S)){ translate += Ogre::Vector3(0, 0, 1); } if(mKeyboard->isKeyDown(OIS::KC_A)){ translate += Ogre::Vector3(-1, 0, 0); } if(mKeyboard->isKeyDown(OIS::KC_D)){ translate += Ogre::Vector3(1, 0, 0); } if(mKeyboard->isKeyDown(OIS::KC_Q)){ translate += mCamera->getOrientation().Inverse() * Ogre::Vector3(0, 1, 0); } if(mKeyboard->isKeyDown(OIS::KC_E)){ translate += mCamera->getOrientation().Inverse() * Ogre::Vector3(0, -1, 0); } Ogre::Real speed = mCamera->getPosition().y; if(speed < 5) speed =5; mCamera->moveRelative(translate * evt.timeSinceLastFrame * speed); if(mKeyboard->isKeyDown(OIS::KC_UP)){ mCamera->pitch(Ogre::Radian(-evt.timeSinceLastFrame)); }else if(mKeyboard->isKeyDown(OIS::KC_DOWN)){ mCamera->pitch(Ogre::Radian(evt.timeSinceLastFrame)); } if(mKeyboard->isKeyDown(OIS::KC_LEFT)){ mCamera->yaw(Ogre::Radian(evt.timeSinceLastFrame)); } if(mKeyboard->isKeyDown(OIS::KC_RIGHT)){ mCamera->yaw(Ogre::Radian(-evt.timeSinceLastFrame * 0.3f)); } mMouse->capture(); Ogre::Real rotX = Ogre::Math::Clamp(mMouse->getMouseState().X.rel * evt.timeSinceLastFrame * -1, -0.1f, 0.1f); Ogre::Real rotY = Ogre::Math::Clamp(mMouse->getMouseState().Y.rel * evt.timeSinceLastFrame * -1, -0.1f, 0.1f); mCamera->yaw(Ogre::Radian(rotX)); mCamera->pitch(Ogre::Radian(rotY)); return true; }
附:代码使用Test/earth原始材质,如果在nasa下载个高清的图片叫earth.jpg,需要指定对应的材质:
material Test/earth
{ technique { pass {
texture_unit
{
texture earth.jpg
}
} } }