mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-22 08:53:54 +00:00
use hardware occlusion query for sun glare effect
This commit is contained in:
parent
5fba52c238
commit
743ea0c9be
7 changed files with 216 additions and 61 deletions
|
@ -2,13 +2,17 @@
|
||||||
|
|
||||||
#include <OgreRenderSystem.h>
|
#include <OgreRenderSystem.h>
|
||||||
#include <OgreRoot.h>
|
#include <OgreRoot.h>
|
||||||
|
#include <OgreBillboardSet.h>
|
||||||
|
|
||||||
using namespace MWRender;
|
using namespace MWRender;
|
||||||
using namespace Ogre;
|
using namespace Ogre;
|
||||||
|
|
||||||
OcclusionQuery::OcclusionQuery() :
|
OcclusionQuery::OcclusionQuery(OEngine::Render::OgreRenderer* renderer, SceneNode* sunNode) :
|
||||||
mSunTotalAreaQuery(0), mSunVisibleAreaQuery(0)
|
mSunTotalAreaQuery(0), mSunVisibleAreaQuery(0), mActiveQuery(0), mDoQuery(0), mSunVisibility(0)
|
||||||
{
|
{
|
||||||
|
mRendering = renderer;
|
||||||
|
mSunNode = sunNode;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RenderSystem* renderSystem = Root::getSingleton().getRenderSystem();
|
RenderSystem* renderSystem = Root::getSingleton().getRenderSystem();
|
||||||
|
|
||||||
|
@ -23,5 +27,113 @@ OcclusionQuery::OcclusionQuery() :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mSupported)
|
if (!mSupported)
|
||||||
|
{
|
||||||
std::cout << "Hardware occlusion queries not supported." << std::endl;
|
std::cout << "Hardware occlusion queries not supported." << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This means that everything up to RENDER_QUEUE_MAIN can occlude the objects that are tested
|
||||||
|
const int queue = RENDER_QUEUE_MAIN+1;
|
||||||
|
|
||||||
|
MaterialPtr matBase = MaterialManager::getSingleton().getByName("BaseWhiteNoLighting");
|
||||||
|
MaterialPtr matQueryArea = matBase->clone("QueryTotalPixels");
|
||||||
|
matQueryArea->setDepthWriteEnabled(false);
|
||||||
|
matQueryArea->setColourWriteEnabled(false);
|
||||||
|
matQueryArea->setDepthCheckEnabled(false); // Not occluded by objects
|
||||||
|
MaterialPtr matQueryVisible = matBase->clone("QueryVisiblePixels");
|
||||||
|
matQueryVisible->setDepthWriteEnabled(false);
|
||||||
|
matQueryVisible->setColourWriteEnabled(false);
|
||||||
|
matQueryVisible->setDepthCheckEnabled(true); // Occluded by objects
|
||||||
|
|
||||||
|
mBBQueryTotal = mRendering->getScene()->createBillboardSet(1);
|
||||||
|
mBBQueryTotal->setDefaultDimensions(150, 150);
|
||||||
|
mBBQueryTotal->createBillboard(Vector3::ZERO);
|
||||||
|
mBBQueryTotal->setMaterialName("QueryTotalPixels");
|
||||||
|
mBBQueryTotal->setRenderQueueGroup(queue);
|
||||||
|
mSunNode->attachObject(mBBQueryTotal);
|
||||||
|
|
||||||
|
mBBQueryVisible = mRendering->getScene()->createBillboardSet(1);
|
||||||
|
mBBQueryVisible->setDefaultDimensions(150, 150);
|
||||||
|
mBBQueryVisible->createBillboard(Vector3::ZERO);
|
||||||
|
mBBQueryVisible->setMaterialName("QueryVisiblePixels");
|
||||||
|
mBBQueryVisible->setRenderQueueGroup(queue);
|
||||||
|
mSunNode->attachObject(mBBQueryVisible);
|
||||||
|
|
||||||
|
mRendering->getScene()->addRenderObjectListener(this);
|
||||||
|
mDoQuery = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OcclusionQuery::~OcclusionQuery()
|
||||||
|
{
|
||||||
|
RenderSystem* renderSystem = Root::getSingleton().getRenderSystem();
|
||||||
|
if (mSunTotalAreaQuery) renderSystem->destroyHardwareOcclusionQuery(mSunTotalAreaQuery);
|
||||||
|
if (mSunVisibleAreaQuery) renderSystem->destroyHardwareOcclusionQuery(mSunVisibleAreaQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OcclusionQuery::supported()
|
||||||
|
{
|
||||||
|
return mSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OcclusionQuery::notifyRenderSingleObject(Renderable* rend, const Pass* pass, const AutoParamDataSource* source,
|
||||||
|
const LightList* pLightList, bool suppressRenderStateChanges)
|
||||||
|
{
|
||||||
|
if (!mSupported) return;
|
||||||
|
|
||||||
|
// The following code activates and deactivates the occlusion queries
|
||||||
|
// so that the queries only include the rendering of their intended targets
|
||||||
|
|
||||||
|
// Close the last occlusion query
|
||||||
|
// Each occlusion query should only last a single rendering
|
||||||
|
if (mActiveQuery != NULL)
|
||||||
|
{
|
||||||
|
mActiveQuery->endOcclusionQuery();
|
||||||
|
mActiveQuery = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a new occlusion query
|
||||||
|
if (mDoQuery == true)
|
||||||
|
{
|
||||||
|
if (rend == mBBQueryTotal)
|
||||||
|
mActiveQuery = mSunTotalAreaQuery;
|
||||||
|
else if (rend == mBBQueryVisible)
|
||||||
|
mActiveQuery = mSunVisibleAreaQuery;
|
||||||
|
|
||||||
|
if (mActiveQuery != NULL)
|
||||||
|
{
|
||||||
|
mActiveQuery->beginOcclusionQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OcclusionQuery::update()
|
||||||
|
{
|
||||||
|
if (!mSupported) return;
|
||||||
|
|
||||||
|
// Stop occlusion queries until we get their information
|
||||||
|
// (may not happen on the same frame they are requested in)
|
||||||
|
mDoQuery = false;
|
||||||
|
|
||||||
|
if (!mSunTotalAreaQuery->isStillOutstanding() && !mSunVisibleAreaQuery->isStillOutstanding())
|
||||||
|
{
|
||||||
|
unsigned int totalPixels;
|
||||||
|
unsigned int visiblePixels;
|
||||||
|
|
||||||
|
mSunTotalAreaQuery->pullOcclusionQuery(&totalPixels);
|
||||||
|
mSunVisibleAreaQuery->pullOcclusionQuery(&visiblePixels);
|
||||||
|
|
||||||
|
if (totalPixels == 0)
|
||||||
|
{
|
||||||
|
// probably outside of the view frustum
|
||||||
|
mSunVisibility = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mSunVisibility = float(visiblePixels) / float(totalPixels);
|
||||||
|
if (mSunVisibility > 1) mSunVisibility = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mDoQuery = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,25 +2,49 @@
|
||||||
#define _GAME_OCCLUSION_QUERY_H
|
#define _GAME_OCCLUSION_QUERY_H
|
||||||
|
|
||||||
#include <OgreHardwareOcclusionQuery.h>
|
#include <OgreHardwareOcclusionQuery.h>
|
||||||
|
#include <OgreRenderObjectListener.h>
|
||||||
|
|
||||||
|
#include <openengine/ogre/renderer.hpp>
|
||||||
|
|
||||||
namespace MWRender
|
namespace MWRender
|
||||||
{
|
{
|
||||||
///
|
///
|
||||||
/// \brief Implements hardware occlusion queries on the GPU
|
/// \brief Implements hardware occlusion queries on the GPU
|
||||||
///
|
///
|
||||||
class OcclusionQuery
|
class OcclusionQuery : public Ogre::RenderObjectListener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
OcclusionQuery();
|
OcclusionQuery(OEngine::Render::OgreRenderer*, Ogre::SceneNode* sunNode);
|
||||||
|
~OcclusionQuery();
|
||||||
|
|
||||||
bool supported();
|
bool supported();
|
||||||
///< returns true if occlusion queries are supported on the user's hardware
|
///< returns true if occlusion queries are supported on the user's hardware
|
||||||
|
|
||||||
|
void update();
|
||||||
|
///< per-frame update
|
||||||
|
|
||||||
|
float getSunVisibility() const {return mSunVisibility;};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ogre::HardwareOcclusionQuery* mSunTotalAreaQuery;
|
Ogre::HardwareOcclusionQuery* mSunTotalAreaQuery;
|
||||||
Ogre::HardwareOcclusionQuery* mSunVisibleAreaQuery;
|
Ogre::HardwareOcclusionQuery* mSunVisibleAreaQuery;
|
||||||
|
Ogre::HardwareOcclusionQuery* mActiveQuery;
|
||||||
|
|
||||||
|
Ogre::BillboardSet* mBBQueryVisible;
|
||||||
|
Ogre::BillboardSet* mBBQueryTotal;
|
||||||
|
|
||||||
|
Ogre::SceneNode* mSunNode;
|
||||||
|
|
||||||
|
float mSunVisibility;
|
||||||
|
|
||||||
bool mSupported;
|
bool mSupported;
|
||||||
|
bool mDoQuery;
|
||||||
|
|
||||||
|
OEngine::Render::OgreRenderer* mRendering;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void notifyRenderSingleObject(Ogre::Renderable* rend, const Ogre::Pass* pass, const Ogre::AutoParamDataSource* source,
|
||||||
|
const Ogre::LightList* pLightList, bool suppressRenderStateChanges);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,11 +55,11 @@ RenderingManager::RenderingManager (OEngine::Render::OgreRenderer& _rend, const
|
||||||
Ogre::SceneNode *cameraPitchNode = cameraYawNode->createChildSceneNode();
|
Ogre::SceneNode *cameraPitchNode = cameraYawNode->createChildSceneNode();
|
||||||
cameraPitchNode->attachObject(mRendering.getCamera());
|
cameraPitchNode->attachObject(mRendering.getCamera());
|
||||||
|
|
||||||
mOcclusionQuery = new OcclusionQuery();
|
|
||||||
|
|
||||||
//mSkyManager = 0;
|
//mSkyManager = 0;
|
||||||
mSkyManager = new SkyManager(mMwRoot, mRendering.getCamera(), &environment);
|
mSkyManager = new SkyManager(mMwRoot, mRendering.getCamera(), &environment);
|
||||||
|
|
||||||
|
mOcclusionQuery = new OcclusionQuery(&mRendering, mSkyManager->getSunNode());
|
||||||
|
|
||||||
mPlayer = new MWRender::Player (mRendering.getCamera(), playerNode);
|
mPlayer = new MWRender::Player (mRendering.getCamera(), playerNode);
|
||||||
mSun = 0;
|
mSun = 0;
|
||||||
|
|
||||||
|
@ -150,8 +150,12 @@ void RenderingManager::update (float duration){
|
||||||
|
|
||||||
mActors.update (duration);
|
mActors.update (duration);
|
||||||
|
|
||||||
|
mOcclusionQuery->update();
|
||||||
|
|
||||||
mSkyManager->update(duration);
|
mSkyManager->update(duration);
|
||||||
|
|
||||||
|
mSkyManager->setGlare(mOcclusionQuery->getSunVisibility());
|
||||||
|
|
||||||
mRendering.update(duration);
|
mRendering.update(duration);
|
||||||
|
|
||||||
mLocalMap->updatePlayer( mRendering.getCamera()->getRealPosition(), mRendering.getCamera()->getRealDirection() );
|
mLocalMap->updatePlayer( mRendering.getCamera()->getRealPosition(), mRendering.getCamera()->getRealDirection() );
|
||||||
|
|
|
@ -99,6 +99,8 @@ class RenderingManager: private RenderingInterface {
|
||||||
void sunEnable();
|
void sunEnable();
|
||||||
void sunDisable();
|
void sunDisable();
|
||||||
|
|
||||||
|
bool occlusionQuerySupported() { return mOcclusionQuery->supported(); };
|
||||||
|
|
||||||
void setGlare(bool glare);
|
void setGlare(bool glare);
|
||||||
void skyEnable ();
|
void skyEnable ();
|
||||||
void skyDisable ();
|
void skyDisable ();
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
#include "../mwworld/environment.hpp"
|
#include "../mwworld/environment.hpp"
|
||||||
#include "../mwworld/world.hpp"
|
#include "../mwworld/world.hpp"
|
||||||
|
#include "occlusionquery.hpp"
|
||||||
|
|
||||||
using namespace MWRender;
|
using namespace MWRender;
|
||||||
using namespace Ogre;
|
using namespace Ogre;
|
||||||
|
@ -30,7 +31,7 @@ BillboardObject::BillboardObject()
|
||||||
|
|
||||||
void BillboardObject::setVisible(const bool visible)
|
void BillboardObject::setVisible(const bool visible)
|
||||||
{
|
{
|
||||||
mNode->setVisible(visible);
|
mBBSet->setVisible(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BillboardObject::setSize(const float size)
|
void BillboardObject::setSize(const float size)
|
||||||
|
@ -88,7 +89,7 @@ void BillboardObject::init(const String& textureName,
|
||||||
/// \todo These billboards are not 100% correct, might want to revisit them later
|
/// \todo These billboards are not 100% correct, might want to revisit them later
|
||||||
mBBSet = sceneMgr->createBillboardSet("SkyBillboardSet"+StringConverter::toString(bodyCount), 1);
|
mBBSet = sceneMgr->createBillboardSet("SkyBillboardSet"+StringConverter::toString(bodyCount), 1);
|
||||||
mBBSet->setDefaultDimensions(550.f*initialSize, 550.f*initialSize);
|
mBBSet->setDefaultDimensions(550.f*initialSize, 550.f*initialSize);
|
||||||
mBBSet->setRenderQueueGroup(RENDER_QUEUE_SKIES_EARLY+2);
|
mBBSet->setRenderQueueGroup(RENDER_QUEUE_MAIN+2);
|
||||||
mBBSet->setBillboardType(BBT_PERPENDICULAR_COMMON);
|
mBBSet->setBillboardType(BBT_PERPENDICULAR_COMMON);
|
||||||
mBBSet->setCommonDirection( -position.normalisedCopy() );
|
mBBSet->setCommonDirection( -position.normalisedCopy() );
|
||||||
mNode = rootNode->createChildSceneNode();
|
mNode = rootNode->createChildSceneNode();
|
||||||
|
@ -293,7 +294,7 @@ void SkyManager::ModVertexAlpha(Entity* ent, unsigned int meshType)
|
||||||
}
|
}
|
||||||
|
|
||||||
SkyManager::SkyManager (SceneNode* pMwRoot, Camera* pCamera, MWWorld::Environment* env) :
|
SkyManager::SkyManager (SceneNode* pMwRoot, Camera* pCamera, MWWorld::Environment* env) :
|
||||||
mGlareFade(0), mGlareEnabled(false)
|
mGlare(0), mGlareFade(0)
|
||||||
{
|
{
|
||||||
mEnvironment = env;
|
mEnvironment = env;
|
||||||
mViewport = pCamera->getViewport();
|
mViewport = pCamera->getViewport();
|
||||||
|
@ -562,10 +563,23 @@ void SkyManager::update(float duration)
|
||||||
mMasser->setPhase( static_cast<Moon::Phase>( (int) ((mDay % 32)/4.f)) );
|
mMasser->setPhase( static_cast<Moon::Phase>( (int) ((mDay % 32)/4.f)) );
|
||||||
mSecunda->setPhase ( static_cast<Moon::Phase>( (int) ((mDay % 32)/4.f)) );
|
mSecunda->setPhase ( static_cast<Moon::Phase>( (int) ((mDay % 32)/4.f)) );
|
||||||
|
|
||||||
// increase the strength of the sun glare effect depending
|
|
||||||
// on how directly the player is looking at the sun
|
|
||||||
if (mSunEnabled)
|
if (mSunEnabled)
|
||||||
{
|
{
|
||||||
|
// take 1/5 sec for fading the glare effect from invisible to full
|
||||||
|
if (mGlareFade > mGlare)
|
||||||
|
{
|
||||||
|
mGlareFade -= duration*5;
|
||||||
|
if (mGlareFade < mGlare) mGlareFade = mGlare;
|
||||||
|
}
|
||||||
|
else if (mGlareFade < mGlare)
|
||||||
|
{
|
||||||
|
mGlareFade += duration*5;
|
||||||
|
if (mGlareFade > mGlare) mGlareFade = mGlare;
|
||||||
|
}
|
||||||
|
|
||||||
|
// increase the strength of the sun glare effect depending
|
||||||
|
// on how directly the player is looking at the sun
|
||||||
Vector3 sun = mSunGlare->getPosition();
|
Vector3 sun = mSunGlare->getPosition();
|
||||||
sun = Vector3(sun.x, sun.z, -sun.y);
|
sun = Vector3(sun.x, sun.z, -sun.y);
|
||||||
Vector3 cam = mViewport->getCamera()->getRealDirection();
|
Vector3 cam = mViewport->getCamera()->getRealDirection();
|
||||||
|
@ -573,21 +587,10 @@ void SkyManager::update(float duration)
|
||||||
float val = 1- (angle.valueDegrees() / 180.f);
|
float val = 1- (angle.valueDegrees() / 180.f);
|
||||||
val = (val*val*val*val)*2;
|
val = (val*val*val*val)*2;
|
||||||
|
|
||||||
if (mGlareEnabled)
|
mSunGlare->setSize(val * mGlareFade);
|
||||||
{
|
|
||||||
mGlareFade += duration*3;
|
|
||||||
if (mGlareFade > 1) mGlareFade = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mGlareFade -= duration*3;
|
|
||||||
if (mGlareFade < 0.3) mGlareFade = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
mSunGlare->setSize(val * (mGlareFade));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mSunGlare->setVisible(mGlareFade>0 && mSunEnabled);
|
mSunGlare->setVisible(mSunEnabled);
|
||||||
mSun->setVisible(mSunEnabled);
|
mSun->setVisible(mSunEnabled);
|
||||||
mMasser->setVisible(mMasserEnabled);
|
mMasser->setVisible(mMasserEnabled);
|
||||||
mSecunda->setVisible(mSecundaEnabled);
|
mSecunda->setVisible(mSecundaEnabled);
|
||||||
|
@ -689,15 +692,15 @@ void SkyManager::setWeather(const MWWorld::WeatherResult& weather)
|
||||||
else
|
else
|
||||||
strength = 1.f;
|
strength = 1.f;
|
||||||
|
|
||||||
mSunGlare->setVisibility(weather.mGlareView * strength);
|
mSunGlare->setVisibility(weather.mGlareView * mGlareFade * strength);
|
||||||
mSun->setVisibility(strength);
|
mSun->setVisibility(mGlareFade >= 0.5 ? weather.mGlareView * mGlareFade * strength : 0);
|
||||||
|
|
||||||
mAtmosphereNight->setVisible(weather.mNight && mEnabled);
|
mAtmosphereNight->setVisible(weather.mNight && mEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SkyManager::setGlare(bool glare)
|
void SkyManager::setGlare(const float glare)
|
||||||
{
|
{
|
||||||
mGlareEnabled = glare;
|
mGlare = glare;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3 SkyManager::getRealSunPos()
|
Vector3 SkyManager::getRealSunPos()
|
||||||
|
@ -782,3 +785,8 @@ void SkyManager::setDate(int day, int month)
|
||||||
mDay = day;
|
mDay = day;
|
||||||
mMonth = month;
|
mMonth = month;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ogre::SceneNode* SkyManager::getSunNode()
|
||||||
|
{
|
||||||
|
return mSun->getNode();
|
||||||
|
}
|
||||||
|
|
|
@ -138,6 +138,8 @@ namespace MWRender
|
||||||
|
|
||||||
void setWeather(const MWWorld::WeatherResult& weather);
|
void setWeather(const MWWorld::WeatherResult& weather);
|
||||||
|
|
||||||
|
Ogre::SceneNode* getSunNode();
|
||||||
|
|
||||||
void sunEnable();
|
void sunEnable();
|
||||||
|
|
||||||
void sunDisable();
|
void sunDisable();
|
||||||
|
@ -160,7 +162,7 @@ namespace MWRender
|
||||||
|
|
||||||
void setThunder(const float factor);
|
void setThunder(const float factor);
|
||||||
|
|
||||||
void setGlare(bool glare);
|
void setGlare(const float glare);
|
||||||
Ogre::Vector3 getRealSunPos();
|
Ogre::Vector3 getRealSunPos();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -203,12 +205,12 @@ namespace MWRender
|
||||||
|
|
||||||
float mRemainingTransitionTime;
|
float mRemainingTransitionTime;
|
||||||
|
|
||||||
float mGlareFade;
|
float mGlare; // target
|
||||||
|
float mGlareFade; // actual
|
||||||
|
|
||||||
void ModVertexAlpha(Ogre::Entity* ent, unsigned int meshType);
|
void ModVertexAlpha(Ogre::Entity* ent, unsigned int meshType);
|
||||||
|
|
||||||
bool mEnabled;
|
bool mEnabled;
|
||||||
bool mGlareEnabled;
|
|
||||||
bool mSunEnabled;
|
bool mSunEnabled;
|
||||||
bool mMasserEnabled;
|
bool mMasserEnabled;
|
||||||
bool mSecundaEnabled;
|
bool mSecundaEnabled;
|
||||||
|
|
|
@ -705,13 +705,16 @@ namespace MWWorld
|
||||||
|
|
||||||
mWeatherManager->update (duration);
|
mWeatherManager->update (duration);
|
||||||
|
|
||||||
// cast a ray from player to sun to detect if the sun is visible
|
if (!mRendering->occlusionQuerySupported())
|
||||||
// this is temporary until we find a better place to put this code
|
{
|
||||||
// currently its here because we need to access the physics system
|
// cast a ray from player to sun to detect if the sun is visible
|
||||||
float* p = mPlayer->getPlayer().getRefData().getPosition().pos;
|
// this is temporary until we find a better place to put this code
|
||||||
Vector3 sun = mRendering->getSkyManager()->getRealSunPos();
|
// currently its here because we need to access the physics system
|
||||||
sun = Vector3(sun.x, -sun.z, sun.y);
|
float* p = mPlayer->getPlayer().getRefData().getPosition().pos;
|
||||||
mRendering->getSkyManager()->setGlare(!mPhysics->castRay(Ogre::Vector3(p[0], p[1], p[2]), sun));
|
Vector3 sun = mRendering->getSkyManager()->getRealSunPos();
|
||||||
|
sun = Vector3(sun.x, -sun.z, sun.y);
|
||||||
|
mRendering->getSkyManager()->setGlare(!mPhysics->castRay(Ogre::Vector3(p[0], p[1], p[2]), sun));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool World::isCellExterior() const
|
bool World::isCellExterior() const
|
||||||
|
|
Loading…
Reference in a new issue