// NeL - MMORPG Framework // Copyright (C) 2010 Winch Gate Property Limited // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include "std3d.h" #include "nel/3d/flare_model.h" #include "nel/3d/flare_shape.h" #include "nel/3d/driver.h" #include "nel/3d/material.h" #include "nel/3d/dru.h" #include "nel/3d/scene.h" #include "nel/3d/render_trav.h" #include "nel/3d/occlusion_query.h" #include "nel/3d/mesh.h" #include "nel/3d/viewport.h" #include "nel/misc/common.h" namespace NL3D { CMaterial CFlareModel::_OcclusionQueryMaterial; CMaterial CFlareModel::_DrawQueryMaterial; bool CFlareModel::_OcclusionQuerySettuped = false; CVertexBuffer CFlareModel::_OcclusionQueryVB; using NLMISC::CVector; // ******************************************************************************************************************** CFlareModel::CFlareModel() { std::fill(_Intensity, _Intensity + MaxNumContext, 0.f); setTransparency(true); setOpacity(false); // RenderFilter: We are a flare _RenderFilterType= UScene::FilterFlare; resetOcclusionQuerries(); std::fill(_LastRenderIntervalBegin, _LastRenderIntervalBegin + MaxNumContext, (uint64) -2); std::fill(_LastRenderIntervalEnd, _LastRenderIntervalEnd + MaxNumContext, (uint64) -2); std::fill(_NumFrameForOcclusionQuery, _NumFrameForOcclusionQuery + MaxNumContext, 1); Next = NULL; } // ******************************************************************************************************************** void CFlareModel::resetOcclusionQuerries() { for(uint k = 0; k < MaxNumContext; ++k) { for(uint l = 0; l < OcclusionTestFrameDelay; ++l) { _OcclusionQuery[k][l] = NULL; _DrawQuery[k][l] = NULL; } } } // ******************************************************************************************************************** CFlareModel::~CFlareModel() { // if driver hasn't changed, delete all querries if (_LastDrv) { for(uint k = 0; k < MaxNumContext; ++k) { for(uint l = 0; l < OcclusionTestFrameDelay; ++l) { if (_OcclusionQuery[k][l]) { _LastDrv->deleteOcclusionQuery(_OcclusionQuery[k][l]); } if (_DrawQuery[k][l]) { _LastDrv->deleteOcclusionQuery(_DrawQuery[k][l]); } } } } } // ******************************************************************************************************************** void CFlareModel::registerBasic() { // register the model CScene::registerModel(FlareModelClassId, TransformShapeId, CFlareModel::creator); } // write a vector in a vertex buffer static inline void vbWrite(uint8 *&dest, const CVector &v) { ((float *) dest)[0] = v.x; ((float *) dest)[1] = v.y; ((float *) dest)[2] = v.z; dest += 3 * sizeof(float); } // write uvs in a vertex buffer static inline void vbWrite(uint8 *&dest, float uCoord, float vCoord) { ((float *) dest)[0] = uCoord; ((float *) dest)[1] = vCoord; dest += 2 * sizeof(float); } // ******************************************************************************************************************** void CFlareModel::traverseRender() { CRenderTrav &renderTrav = getOwnerScene()->getRenderTrav(); if (renderTrav.isCurrentPassOpaque()) return; IDriver *drv = renderTrav.getDriver(); nlassert(drv); // For now, don't render flare if occlusion query is not supported (direct read of z-buffer is far too slow) if (!drv->supportOcclusionQuery()) return; if (drv != _LastDrv) { // occlusion queries have been deleted by the driver resetOcclusionQuerries(); _LastDrv = drv; } uint flareContext = _Scene ? _Scene->getFlareContext() : 0; // transform the flare on screen const CVector upt = getWorldMatrix().getPos(); // untransformed pos const CVector pt = renderTrav.ViewMatrix * upt; if (pt.y <= renderTrav.Near) { return; // flare behind us } nlassert(Shape); CFlareShape *fs = NLMISC::safe_cast((IShape *) Shape); if (pt.y > fs->getMaxViewDist()) { return; // flare too far away } float distIntensity; if (fs->getFlareAtInfiniteDist()) { distIntensity = 1.f; } else { // compute a color ratio for attenuation with distance const float distRatio = pt.y / fs->getMaxViewDist(); distIntensity = distRatio > fs->getMaxViewDistRatio() ? 1.f - (distRatio - fs->getMaxViewDistRatio()) / (1.f - fs->getMaxViewDistRatio()) : 1.f; } // uint32 width, height; drv->getWindowSize(width, height); // Compute position on screen const float middleX = .5f * (renderTrav.Left + renderTrav.Right); const float middleZ = .5f * (renderTrav.Bottom + renderTrav.Top); const sint xPos = (width>>1) + (sint) (width * (((renderTrav.Near * pt.x) / pt.y) - middleX) / (renderTrav.Right - renderTrav.Left)); const sint yPos = (height>>1) - (sint) (height * (((renderTrav.Near * pt.z) / pt.y) - middleZ) / (renderTrav.Top - renderTrav.Bottom)); // See if the flare was inside the frustum during the last frame // We can't use the scene frame counter because a flare can be rendered in several viewport during the same frame // The swapBuffer counter is called only once per frame uint64 currFrame = drv->getSwapBufferCounter(); // bool visibilityRetrieved = false; float visibilityRatio = 0.f; // if driver support occlusion query mechanism, use it CMesh *occlusionTestMesh = NULL; if (_Scene->getShapeBank()) { occlusionTestMesh = fs->getOcclusionTestMesh(*_Scene->getShapeBank()); } if (drv->supportOcclusionQuery()) { bool issueNewQuery = true; IOcclusionQuery *lastOQ = _OcclusionQuery[flareContext][OcclusionTestFrameDelay - 1]; IOcclusionQuery *lastDQ = _DrawQuery[flareContext][OcclusionTestFrameDelay - 1]; if (_LastRenderIntervalEnd[flareContext] + 1 == currFrame) { if (_LastRenderIntervalEnd[flareContext] - _LastRenderIntervalBegin[flareContext] >= OcclusionTestFrameDelay - 1) { // occlusion test are possibles if at least OcclusionTestFrameDelay frames have ellapsed if (lastOQ) { switch(lastOQ->getOcclusionType()) { case IOcclusionQuery::NotAvailable: issueNewQuery = false; ++ _NumFrameForOcclusionQuery[flareContext]; break; case IOcclusionQuery::Occluded: visibilityRetrieved = true; visibilityRatio = 0.f; break; case IOcclusionQuery::NotOccluded: if (occlusionTestMesh) { if (lastDQ) { if (lastDQ->getOcclusionType() != IOcclusionQuery::NotAvailable) { visibilityRetrieved = true; // eval the percentage of samples that are visible //nlinfo("%d / %d", lastOQ->getVisibleCount(), lastDQ->getVisibleCount()); visibilityRatio = (float) lastOQ->getVisibleCount() / (float) lastDQ->getVisibleCount(); NLMISC::clamp(visibilityRatio, 0.f, 1.f); } } else { visibilityRetrieved = true; visibilityRatio = 1.f; } } else { // visibility test is done on a single point visibilityRetrieved = true; visibilityRatio = 1.f; } break; } } } } if (issueNewQuery) { // shift the queries list for(uint k = OcclusionTestFrameDelay - 1; k > 0; --k) { _OcclusionQuery[flareContext][k] = _OcclusionQuery[flareContext][k - 1]; _DrawQuery[flareContext][k] = _DrawQuery[flareContext][k - 1]; } _OcclusionQuery[flareContext][0] = lastOQ; _DrawQuery[flareContext][0] = lastDQ; if (occlusionTestMesh) { occlusionTest(*occlusionTestMesh, *drv); } else { // Insert in list of waiting flare. Don't do it now to avoid repeated setup of test material (a material that don't write to color/zbuffer, // and that is used for the sole purpose of the occlusion query) _Scene->insertInOcclusionQueryList(this); } } } else { _NumFrameForOcclusionQuery[flareContext] = 1; visibilityRetrieved = true; // The device doesn't support asynchronous query -> must read the z-buffer directly in a slow fashion CViewport vp; drv->getViewport(vp); // Read z-buffer value at the pos we are static std::vector v(1); NLMISC::CRect rect((sint32) (vp.getX() * width + vp.getWidth() * xPos), (sint32) (vp.getY() * height + vp.getHeight() * (height - yPos)), 1, 1); drv->getZBufferPart(v, rect); // Project in screen space float z = (float) (1.0 - (1.0 / pt.y - 1.0 / renderTrav.Far) / (1.0 /renderTrav.Near - 1.0 / renderTrav.Far)); // float depthRangeNear, depthRangeFar; drv->getDepthRange(depthRangeNear, depthRangeFar); z = (depthRangeFar - depthRangeNear) * z + depthRangeNear; if (!v.size() || z > v[0]) // test against z-buffer { visibilityRatio = 0.f; } else { visibilityRatio = 1.f; } } // Update render interval // nlwarning("frame = %d, last frame = %d", (int) currFrame, (int) _LastRenderIntervalEnd[flareContext]); if (_LastRenderIntervalEnd[flareContext] + 1 != currFrame) { //nlwarning("*"); _Intensity[flareContext] = 0.f; _LastRenderIntervalBegin[flareContext] = currFrame; } _LastRenderIntervalEnd[flareContext] = currFrame; // Update intensity depending on visibility if (visibilityRetrieved) { nlassert(visibilityRatio >= 0.f); nlassert(visibilityRatio <= 1.f); _NumFrameForOcclusionQuery[flareContext] = 1; // reset number of frame needed to do the occlusion query if (visibilityRatio < _Intensity[flareContext]) { float p = fs->getPersistence(); if (p == 0.f) { _Intensity[flareContext] = visibilityRatio; // instant update } else { _Intensity[flareContext] -= 1.f / p * (float)_Scene->getEllapsedTime() * (float) _NumFrameForOcclusionQuery[flareContext]; if (_Intensity[flareContext] < visibilityRatio) { _Intensity[flareContext] = visibilityRatio; } } //nlwarning("intensity update < of %x : %f", (int) this, _Intensity[flareContext]); } else if (visibilityRatio > _Intensity[flareContext]) { float p = fs->getPersistence(); if (p == 0.f) { _Intensity[flareContext] = visibilityRatio; // instant update } else { //nlwarning("num frame = %d, currFrame = %d, ", (int) _NumFrameForOcclusionQuery[flareContext], (int) currFrame); _Intensity[flareContext] += 1.f / p * (float)_Scene->getEllapsedTime() * (float) _NumFrameForOcclusionQuery[flareContext]; if (_Intensity[flareContext] > visibilityRatio) { _Intensity[flareContext] = visibilityRatio; } } //nlwarning("intensity update > of %x : %f", (int) this, _Intensity[flareContext]); } } if (_Intensity[flareContext] == 0.f) return; // static CMaterial material; static CVertexBuffer vb; static bool setupDone = false; if (!setupDone) { material.setBlend(true); material.setBlendFunc(CMaterial::one, CMaterial::one); material.setZWrite(false); material.setZFunc(CMaterial::always); material.setLighting(false); material.setDoubleSided(true); // setup vertex buffer vb.setVertexFormat(CVertexBuffer::PositionFlag | CVertexBuffer::TexCoord0Flag); vb.setPreferredMemory(CVertexBuffer::RAMVolatile, false); vb.setNumVertices(4); vb.setName("CFlareModel"); { CVertexBufferReadWrite vba; vb.lock (vba); vba.setTexCoord(0, 0, NLMISC::CUV(1, 0)); vba.setTexCoord(1, 0, NLMISC::CUV(1, 1)); vba.setTexCoord(2, 0, NLMISC::CUV(0, 1)); vba.setTexCoord(3, 0, NLMISC::CUV(0, 0)); } setupDone = true; } // setup driver drv->activeVertexProgram(NULL); drv->setupModelMatrix(fs->getLookAtMode() ? CMatrix::Identity : getWorldMatrix()); // we don't change the fustrum to draw 2d shapes : it is costly, and we need to restore it after the drawing has been done // we setup Z to be (near + far) / 2, and setup x and y to get the screen coordinates we want const float zPos = 0.5f * (renderTrav.Near + renderTrav.Far); const float zPosDivNear = zPos / renderTrav.Near; // compute the coeff so that x = ax * px + bx; y = ax * py + by const float aX = ( (renderTrav.Right - renderTrav.Left) / (float) width) * zPosDivNear; const float bX = zPosDivNear * (middleX - 0.5f * (renderTrav.Right - renderTrav.Left)); // const float aY = - ( (renderTrav.Top - renderTrav.Bottom) / (float) height) * zPosDivNear; const float bY = zPosDivNear * (middleZ + 0.5f * (renderTrav.Top - renderTrav.Bottom)); const CVector I = renderTrav.CamMatrix.getI(); const CVector J = renderTrav.CamMatrix.getJ(); const CVector K = renderTrav.CamMatrix.getK(); // CRGBA col; CRGBA flareColor = fs->getColor(); const float norm = sqrtf((float) (((xPos - (width>>1)) * (xPos - (width>>1)) + (yPos - (height>>1))*(yPos - (height>>1))))) / (float) (width>>1); // check for dazzle and draw it /*if (fs->hasDazzle()) { if (norm < fs->getDazzleAttenuationRange()) { float dazzleIntensity = 1.f - norm / fs->getDazzleAttenuationRange(); CRGBA dazzleColor = fs->getDazzleColor(); col.modulateFromui(dazzleColor, (uint) (255.f * _Intensity * dazzleIntensity)); material.setColor(col); material.setTexture(0, NULL); const CVector dazzleCenter = renderTrav.CamPos + zPos * J; const CVector dI = (width>>1) * aX * I; const CVector dK = (height>>1) * bX * K; vb.setVertexCoord(0, dazzleCenter + dI + dK); vb.setVertexCoord(1, dazzleCenter + dI - dK); vb.setVertexCoord(2, dazzleCenter - dI - dK); vb.setVertexCoord(3, dazzleCenter - dI + dK); drv->renderRawQuads(material, 0, 1); } } */ if (!fs->getAttenuable() ) { col.modulateFromui(flareColor, (uint) (255.f * distIntensity * _Intensity[flareContext])); } else { if (norm > fs->getAttenuationRange() || fs->getAttenuationRange() == 0.f) { return; // nothing to draw; } col.modulateFromui(flareColor, (uint) (255.f * distIntensity * _Intensity[flareContext] * (1.f - norm / fs->getAttenuationRange() ))); } col.modulateFromColor(col, getMeanColor()); if (col == CRGBA::Black) return; // not visible material.setColor(col); CVector scrPos; // vector that will map to the center of the flare on screen // process each flare // delta for each new Pos const float dX = fs->getFlareSpacing() * ((sint) (width >> 1) - xPos); const float dY = fs->getFlareSpacing() * ((sint) (height >> 1) - yPos); ITexture *tex; // special case for first flare tex = fs->getTexture(0); if (tex) { { CVertexBufferReadWrite vba; vb.lock (vba); float size; if (fs->getScaleWhenDisappear()) { size = _Intensity[flareContext] * fs->getSize(0) + (1.f - _Intensity[flareContext]) * fs->getSizeDisappear(); } else { size = fs->getSize(0); } CVector rI, rK; if (fs->getFirstFlareKeepSize()) { size *= renderTrav.Near * (getWorldMatrix().getPos() - renderTrav.CamMatrix.getPos()) * J; } if (fs->getAngleDisappear() == 0.f) { if (fs->getLookAtMode()) { rI = I; rK = K; } else { rI = NLMISC::CVector::I; rK = NLMISC::CVector::K; } } else { float angle = (1.f - _Intensity[flareContext]) * fs->getAngleDisappear() * (float) (NLMISC::Pi / 180); float cosTheta = cosf(angle); float sinTheta = sinf(angle); if (fs->getLookAtMode()) { rI = cosTheta * I + sinTheta * K; rK = -sinTheta * I + cosTheta * K; } else { rI.set(cosTheta, 0.f, sinTheta); rK.set(-sinTheta, 0.f, cosTheta); } } uint8 *vbPtr = (uint8 *) vba.getVertexCoordPointer(); CHECK_VBA_RANGE(vba, vbPtr, vb.getVertexSize()); if (fs->getLookAtMode()) { CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, upt + size * (rI + rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 1.f, 0.f); // uvs CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, upt + size * (rI - rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 1.f, 1.f); // uvs CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, upt + size * (-rI - rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 0.f, 1.f); // uvs CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, upt + size * (-rI + rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 0.f, 0.f); // uvs } else { CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, size * (rI + rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 1.f, 0.f); // uvs CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, size * (rI - rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 1.f, 1.f); // uvs CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, size * (-rI - rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 0.f, 1.f); // uvs CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, size * (-rI + rK)); CHECK_VBA(vba, vbPtr); vbWrite(vbPtr, 0.f, 0.f); // uvs } } material.setTexture(0, tex); drv->activeVertexBuffer(vb); drv->renderRawQuads(material, 0, 1); } if (fs->_LookAtMode) { drv->setupModelMatrix(CMatrix::Identity); // look at mode is applied only to first flare } for (uint k = 1; k < MaxFlareNum; ++k) { tex = fs->getTexture(k); if (tex) { // compute vector that map to the center of the flare scrPos = (aX * (xPos + dX * fs->getRelativePos(k)) + bX) * I + zPos * J + (aY * (yPos + dY * fs->getRelativePos(k)) + bY) * K + renderTrav.CamMatrix.getPos(); { CVertexBufferReadWrite vba; vb.lock (vba); uint8 *vbPtr = (uint8 *) vba.getVertexCoordPointer(); float size = fs->getSize(k) * zPos * renderTrav.Near; vbWrite(vbPtr, scrPos + size * (I + K)); vbWrite(vbPtr, 1.f, 0.f); // uvs vbWrite(vbPtr, scrPos + size * (I - K)); vbWrite(vbPtr, 1.f, 1.f); // uvs vbWrite(vbPtr, scrPos + size * (-I - K)); vbWrite(vbPtr, 0.f, 1.f); // uvs vbWrite(vbPtr, scrPos + size * (-I + K)); vbWrite(vbPtr, 0.f, 0.f); // uvs } material.setTexture(0, tex); drv->activeVertexBuffer(vb); drv->renderRawQuads(material, 0, 1); } } } // ******************************************************************************************************************** void CFlareModel::initStatics() { if (!_OcclusionQuerySettuped) { // setup materials _OcclusionQueryMaterial.initUnlit(); _OcclusionQueryMaterial.setZWrite(false); _DrawQueryMaterial.initUnlit(); _DrawQueryMaterial.setZWrite(false); _DrawQueryMaterial.setZFunc(CMaterial::always); // setup vbs _OcclusionQueryVB.setVertexFormat(CVertexBuffer::PositionFlag); _OcclusionQueryVB.setName("CFlareModel::_OcclusionQueryVB"); _OcclusionQueryVB.setPreferredMemory(CVertexBuffer::RAMVolatile, false); // use ram to avoid stall, and don't want to setup a VB per flare! _OcclusionQueryVB.setNumVertices(1); _OcclusionQuerySettuped = true; } } // ******************************************************************************************************************** void CFlareModel::updateOcclusionQueryBegin(IDriver *drv) { nlassert(drv); drv->activeVertexProgram(NULL); drv->setupModelMatrix(CMatrix::Identity); initStatics(); drv->setColorMask(false, false, false, false); // don't write any pixel during the test } // ******************************************************************************************************************** void CFlareModel::updateOcclusionQueryEnd(IDriver *drv) { drv->setColorMask(true, true, true, true); } // ******************************************************************************************************************** void CFlareModel::updateOcclusionQuery(IDriver *drv) { nlassert(drv); nlassert(drv == _LastDrv); // driver shouldn't change during CScene::render // allocate a new occlusion if nit already done nlassert(_Scene); IOcclusionQuery *oq = _OcclusionQuery[_Scene->getFlareContext()][0]; if (!oq) { nlassert(drv->supportOcclusionQuery()); oq = drv->createOcclusionQuery(); if (!oq) return; _OcclusionQuery[_Scene->getFlareContext()][0] = oq; } { CVertexBufferReadWrite vbrw; _OcclusionQueryVB.lock(vbrw); *vbrw.getVertexCoordPointer(0) = getWorldMatrix().getPos(); } drv->activeVertexBuffer(_OcclusionQueryVB); oq->begin(); // draw a single point drv->renderRawPoints(_OcclusionQueryMaterial, 0, 1); oq->end(); } // ******************************************************************************************************************** void CFlareModel::renderOcclusionMeshPrimitives(CMesh &mesh, IDriver &drv) { uint numMatrixBlock = mesh.getNbMatrixBlock(); for(uint k = 0; k < numMatrixBlock; ++k) { uint numRdrPass = mesh.getNbRdrPass(k); for(uint l = 0; l < numRdrPass; ++l) { CIndexBuffer &ib = const_cast(mesh.getRdrPassPrimitiveBlock(k, l)); drv.activeIndexBuffer(ib); drv.renderSimpleTriangles(0, ib.getNumIndexes() / 3); } } } // ******************************************************************************************************************** void CFlareModel::setupOcclusionMeshMatrix(IDriver &drv, CScene &scene) const { nlassert(Shape); CFlareShape *fs = NLMISC::safe_cast((IShape *) Shape); if (fs->getOcclusionTestMeshInheritScaleRot()) { drv.setupModelMatrix(getWorldMatrix()); } else { nlassert(scene.getCam()); CMatrix m = scene.getCam()->getWorldMatrix(); m.setPos(getWorldMatrix().getPos()); drv.setupModelMatrix(m); } } // ******************************************************************************************************************** void CFlareModel::occlusionTest(CMesh &mesh, IDriver &drv) { nlassert(_Scene); initStatics(); IOcclusionQuery *oq = _OcclusionQuery[_Scene->getFlareContext()][0]; if (!oq) { nlassert(drv.supportOcclusionQuery()); oq = drv.createOcclusionQuery(); if (!oq) return; _OcclusionQuery[_Scene->getFlareContext()][0] = oq; } IOcclusionQuery *dq = _DrawQuery[_Scene->getFlareContext()][0]; if (!dq) { nlassert(drv.supportOcclusionQuery()); dq = drv.createOcclusionQuery(); if (!dq) return; _DrawQuery[_Scene->getFlareContext()][0] = dq; } drv.setColorMask(false, false, false, false); // don't write any pixel during the test drv.activeVertexProgram(NULL); setupOcclusionMeshMatrix(drv, *_Scene); drv.activeVertexBuffer(const_cast(mesh.getVertexBuffer())); // query drawn count drv.setupMaterial(_OcclusionQueryMaterial); oq->begin(); renderOcclusionMeshPrimitives(mesh, drv); oq->end(); // query total count drv.setupMaterial(_DrawQueryMaterial); dq->begin(); renderOcclusionMeshPrimitives(mesh, drv); dq->end(); drv.setColorMask(true, true, true, true); // restore pixel writes } // ******************************************************************************************************************** void CFlareModel::renderOcclusionTestMesh(IDriver &drv) { nlassert(_Scene); if (!_Scene->getShapeBank()) return; nlassert(Shape); CFlareShape *fs = NLMISC::safe_cast((IShape *) Shape); CMesh *occlusionTestMesh = fs->getOcclusionTestMesh(*_Scene->getShapeBank()); if (!occlusionTestMesh) return; setupOcclusionMeshMatrix(drv, *_Scene); drv.activeVertexBuffer(const_cast(occlusionTestMesh->getVertexBuffer())); renderOcclusionMeshPrimitives(*occlusionTestMesh, drv); } } // NL3D