mirror of
https://github.com/overte-org/overte.git
synced 2025-07-23 10:23:59 +02:00
Just need to write correct textureLod equivalent on CPU cube map
This commit is contained in:
parent
a39fe7452c
commit
4a2323f3c2
5 changed files with 280 additions and 39 deletions
|
@ -10,6 +10,16 @@
|
||||||
//
|
//
|
||||||
#include "CubeMap.h"
|
#include "CubeMap.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/blocked_range2d.h>
|
||||||
|
|
||||||
|
#include "RandomAndNoise.h"
|
||||||
|
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265359
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace image;
|
using namespace image;
|
||||||
|
|
||||||
CubeMap::CubeMap(int width, int height, int mipCount) :
|
CubeMap::CubeMap(int width, int height, int mipCount) :
|
||||||
|
@ -26,3 +36,207 @@ CubeMap::CubeMap(int width, int height, int mipCount) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glm::vec4 CubeMap::fetchLod(const glm::vec3& dir, float lod) const {
|
||||||
|
// TODO
|
||||||
|
return glm::vec4(0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static glm::vec3 sampleGGX(const glm::vec2& Xi, const float roughness) {
|
||||||
|
const float a = roughness * roughness;
|
||||||
|
|
||||||
|
float phi = (float)(2.0 * M_PI * Xi.x);
|
||||||
|
float cosTheta = (float)(std::sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)));
|
||||||
|
float sinTheta = (float)(std::sqrt(1.0 - cosTheta * cosTheta));
|
||||||
|
|
||||||
|
// from spherical coordinates to cartesian coordinates
|
||||||
|
glm::vec3 H;
|
||||||
|
H.x = std::cos(phi) * sinTheta;
|
||||||
|
H.y = std::sin(phi) * sinTheta;
|
||||||
|
H.z = cosTheta;
|
||||||
|
|
||||||
|
return H;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float evaluateGGX(float NdotH, float roughness) {
|
||||||
|
float alpha = roughness * roughness;
|
||||||
|
float alphaSquared = alpha * alpha;
|
||||||
|
float denom = (float)(NdotH * NdotH * (alphaSquared - 1.0) + 1.0);
|
||||||
|
return alphaSquared / (denom * denom);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CubeMap::GGXSamples {
|
||||||
|
float invTotalWeight;
|
||||||
|
std::vector<glm::vec4> points;
|
||||||
|
};
|
||||||
|
|
||||||
|
void CubeMap::generateGGXSamples(GGXSamples& data, float roughness, const int resolution) {
|
||||||
|
glm::vec2 xi;
|
||||||
|
glm::vec3 L;
|
||||||
|
glm::vec3 H;
|
||||||
|
const float saTexel = (float)(4.0 * M_PI / (6.0 * resolution * resolution));
|
||||||
|
const float mipBias = 3.0f;
|
||||||
|
const auto sampleCount = data.points.size();
|
||||||
|
const auto hammersleySequenceLength = data.points.size();
|
||||||
|
int sampleIndex = 0;
|
||||||
|
int hammersleySampleIndex = 0;
|
||||||
|
float NdotL;
|
||||||
|
|
||||||
|
data.invTotalWeight = 0.0f;
|
||||||
|
|
||||||
|
// Do some computation in tangent space
|
||||||
|
while (sampleIndex < sampleCount) {
|
||||||
|
if (hammersleySampleIndex < hammersleySequenceLength) {
|
||||||
|
xi = evaluateHammersley((int)hammersleySampleIndex, (int)hammersleySequenceLength);
|
||||||
|
H = sampleGGX(xi, roughness);
|
||||||
|
L = H * (2.0f * H.z) - glm::vec3(0.0f, 0.0f, 1.0f);
|
||||||
|
NdotL = L.z;
|
||||||
|
hammersleySampleIndex++;
|
||||||
|
} else {
|
||||||
|
NdotL = -1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (NdotL <= 0.0f) {
|
||||||
|
// Create a purely random sample
|
||||||
|
xi.x = rand() / float(RAND_MAX);
|
||||||
|
xi.y = rand() / float(RAND_MAX);
|
||||||
|
H = sampleGGX(xi, roughness);
|
||||||
|
L = H * (2.0f * H.z) - glm::vec3(0.0f, 0.0f, 1.0f);
|
||||||
|
NdotL = L.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
float NdotH = std::max(0.0f, H.z);
|
||||||
|
float HdotV = NdotH;
|
||||||
|
float D = evaluateGGX(NdotH, roughness);
|
||||||
|
float pdf = (D * NdotH / (4.0f * HdotV)) + 0.0001f;
|
||||||
|
float saSample = 1.0f / (float(sampleCount) * pdf + 0.0001f);
|
||||||
|
float mipLevel = std::max(0.5f * log2(saSample / saTexel) + mipBias, 0.0f);
|
||||||
|
|
||||||
|
auto& sample = data.points[sampleIndex];
|
||||||
|
sample.x = L.x;
|
||||||
|
sample.y = L.y;
|
||||||
|
sample.z = L.z;
|
||||||
|
sample.w = mipLevel;
|
||||||
|
|
||||||
|
data.invTotalWeight += NdotL;
|
||||||
|
|
||||||
|
sampleIndex++;
|
||||||
|
}
|
||||||
|
data.invTotalWeight = 1.0f / data.invTotalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeMap::convolveForGGX(CubeMap& output, const std::atomic<bool>& abortProcessing) const {
|
||||||
|
// This should match fragment.glsl values, too
|
||||||
|
static const float ROUGHNESS_1_MIP_RESOLUTION = 1.5f;
|
||||||
|
static const gpu::uint16 MAX_SAMPLE_COUNT = 4000;
|
||||||
|
|
||||||
|
const auto mipCount = getMipCount();
|
||||||
|
GGXSamples params;
|
||||||
|
|
||||||
|
params.points.reserve(MAX_SAMPLE_COUNT);
|
||||||
|
|
||||||
|
for (gpu::uint16 mipLevel = 0; mipLevel < mipCount; ++mipLevel) {
|
||||||
|
// This is the inverse code found in fragment.glsl in evaluateAmbientLighting
|
||||||
|
float levelAlpha = float(mipLevel) / (mipCount - ROUGHNESS_1_MIP_RESOLUTION);
|
||||||
|
float mipRoughness = levelAlpha * (1.0f + 2.0f * levelAlpha) / 3.0f;
|
||||||
|
mipRoughness = std::max(1e-3f, mipRoughness);
|
||||||
|
mipRoughness = std::min(1.0f, mipRoughness);
|
||||||
|
|
||||||
|
params.points.resize(std::min<size_t>(MAX_SAMPLE_COUNT, 1U + size_t(4000 * mipRoughness * mipRoughness)));
|
||||||
|
generateGGXSamples(params, mipRoughness, _width);
|
||||||
|
|
||||||
|
for (int face = 0; face < 6; face++) {
|
||||||
|
convolveMipFaceForGGX(params, output, mipLevel, face, abortProcessing);
|
||||||
|
if (abortProcessing.load()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeMap::convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic<bool>& abortProcessing) const {
|
||||||
|
static const glm::vec3 NORMALS[24] = {
|
||||||
|
// POSITIVE X
|
||||||
|
glm::vec3(1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3(1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3(1.0f, -1.0f, 1.0f),
|
||||||
|
glm::vec3(1.0f, -1.0f, -1.0f),
|
||||||
|
// NEGATIVE X
|
||||||
|
glm::vec3(-1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, 1.0f),
|
||||||
|
// POSITIVE Y
|
||||||
|
glm::vec3(-1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3(1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3(1.0f, 1.0f, 1.0f),
|
||||||
|
// NEGATIVE Y
|
||||||
|
glm::vec3(-1.0f, -1.0f, 1.0f),
|
||||||
|
glm::vec3(1.0f, -1.0f, 1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, -1.0f),
|
||||||
|
glm::vec3(1.0f, -1.0f, -1.0f),
|
||||||
|
// POSITIVE Z
|
||||||
|
glm::vec3(-1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3(1.0f, 1.0f, 1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, 1.0f),
|
||||||
|
glm::vec3(1.0f, -1.0f, 1.0f),
|
||||||
|
// NEGATIVE Z
|
||||||
|
glm::vec3(1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, 1.0f, -1.0f),
|
||||||
|
glm::vec3(1.0f, -1.0f, -1.0f),
|
||||||
|
glm::vec3(-1.0f, -1.0f, -1.0f)
|
||||||
|
};
|
||||||
|
|
||||||
|
const glm::vec3* faceNormals = NORMALS + face * 4;
|
||||||
|
const glm::vec3 deltaXNormalLo = faceNormals[1] - faceNormals[0];
|
||||||
|
const glm::vec3 deltaXNormalHi = faceNormals[3] - faceNormals[2];
|
||||||
|
auto& outputFace = output._mips[mipLevel][face];
|
||||||
|
|
||||||
|
tbb::parallel_for(tbb::blocked_range2d<int, int>(0, _width, 16, 0, _height, 16), [&](const tbb::blocked_range2d<int, int>& range) {
|
||||||
|
auto rowRange = range.rows();
|
||||||
|
auto colRange = range.cols();
|
||||||
|
|
||||||
|
for (auto x = rowRange.begin(); x < rowRange.end(); x++) {
|
||||||
|
const float xAlpha = (x + 0.5f) / _width;
|
||||||
|
const glm::vec3 normalYLo = faceNormals[0] + deltaXNormalLo * xAlpha;
|
||||||
|
const glm::vec3 normalYHi = faceNormals[2] + deltaXNormalHi * xAlpha;
|
||||||
|
const glm::vec3 deltaYNormal = normalYHi - normalYLo;
|
||||||
|
|
||||||
|
for (auto y = colRange.begin(); y < colRange.end(); y++) {
|
||||||
|
const float yAlpha = (y + 0.5f) / _width;
|
||||||
|
// Interpolate normal for this pixel
|
||||||
|
const glm::vec3 normal = glm::normalize(normalYLo + deltaYNormal * yAlpha);
|
||||||
|
|
||||||
|
outputFace[x + y * _width] = computeConvolution(normal, samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abortProcessing.load()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec4 CubeMap::computeConvolution(const glm::vec3& N, const GGXSamples& samples) const {
|
||||||
|
// from tangent-space vector to world-space
|
||||||
|
glm::vec3 bitangent = abs(N.z) < 0.999 ? glm::vec3(0.0, 0.0, 1.0) : glm::vec3(1.0, 0.0, 0.0);
|
||||||
|
glm::vec3 tangent = glm::normalize(glm::cross(bitangent, N));
|
||||||
|
bitangent = glm::cross(N, tangent);
|
||||||
|
|
||||||
|
const size_t sampleCount = samples.points.size();
|
||||||
|
glm::vec4 prefilteredColor = glm::vec4(0.0f);
|
||||||
|
|
||||||
|
for (int i = 0; i < sampleCount; ++i) {
|
||||||
|
const auto& sample = samples.points[i];
|
||||||
|
glm::vec3 L(sample.x, sample.y, sample.z);
|
||||||
|
float NdotL = L.z;
|
||||||
|
float mipLevel = sample.w;
|
||||||
|
// Now back to world space
|
||||||
|
L = tangent * L.x + bitangent * L.y + N * L.z;
|
||||||
|
prefilteredColor += fetchLod(L, mipLevel) * NdotL;
|
||||||
|
}
|
||||||
|
prefilteredColor = prefilteredColor * samples.invTotalWeight;
|
||||||
|
prefilteredColor.a = 1.0f;
|
||||||
|
return prefilteredColor;
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
#include <glm/vec4.hpp>
|
#include <glm/vec4.hpp>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace image {
|
namespace image {
|
||||||
|
|
||||||
|
@ -31,11 +32,20 @@ namespace image {
|
||||||
Faces& editMip(gpu::uint16 mipLevel) { return _mips[mipLevel]; }
|
Faces& editMip(gpu::uint16 mipLevel) { return _mips[mipLevel]; }
|
||||||
const Faces& getMip(gpu::uint16 mipLevel) const { return _mips[mipLevel]; }
|
const Faces& getMip(gpu::uint16 mipLevel) const { return _mips[mipLevel]; }
|
||||||
|
|
||||||
|
void convolveForGGX(CubeMap& output, const std::atomic<bool>& abortProcessing) const;
|
||||||
|
glm::vec4 fetchLod(const glm::vec3& dir, float lod) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
struct GGXSamples;
|
||||||
|
|
||||||
int _width;
|
int _width;
|
||||||
int _height;
|
int _height;
|
||||||
std::vector<Faces> _mips;
|
std::vector<Faces> _mips;
|
||||||
|
|
||||||
|
static void generateGGXSamples(GGXSamples& data, float roughness, const int resolution);
|
||||||
|
void convolveMipFaceForGGX(const GGXSamples& samples, CubeMap& output, gpu::uint16 mipLevel, int face, const std::atomic<bool>& abortProcessing) const;
|
||||||
|
glm::vec4 computeConvolution(const glm::vec3& normal, const GGXSamples& samples) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -850,13 +850,10 @@ void generateMips(gpu::Texture* texture, QImage&& image, BackendTarget target, c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void convolveFaceWithGGX(const CubeMap& source, int face, const std::atomic<bool>& abortProcessing) {
|
void convolveForGGX(gpu::Texture* texture, BackendTarget target, const std::atomic<bool>& abortProcessing = false) {
|
||||||
|
PROFILE_RANGE(resource_parse, "convolveForGGX");
|
||||||
}
|
|
||||||
|
|
||||||
void convolveWithGGX(gpu::Texture* texture, BackendTarget target, const std::atomic<bool>& abortProcessing = false) {
|
|
||||||
PROFILE_RANGE(resource_parse, "convolveWithGGX");
|
|
||||||
CubeMap source(texture->getWidth(), texture->getHeight(), texture->getNumMips());
|
CubeMap source(texture->getWidth(), texture->getHeight(), texture->getNumMips());
|
||||||
|
CubeMap output(texture->getWidth(), texture->getHeight(), texture->getNumMips());
|
||||||
gpu::uint16 mipLevel;
|
gpu::uint16 mipLevel;
|
||||||
int face;
|
int face;
|
||||||
const auto textureFormat = texture->getTexelFormat();
|
const auto textureFormat = texture->getTexelFormat();
|
||||||
|
@ -875,18 +872,16 @@ void convolveWithGGX(gpu::Texture* texture, BackendTarget target, const std::ato
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (face = 0; face < 6; face++) {
|
source.convolveForGGX(output, abortProcessing);
|
||||||
convolveFaceWithGGX(source, face, abortProcessing);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!abortProcessing) {
|
if (!abortProcessing) {
|
||||||
// Convert all mip data back from float
|
// Convert all mip data back from float
|
||||||
unsigned char* convertedPixels = new unsigned char[texture->getWidth() * texture->getHeight() * sizeof(uint32)];
|
unsigned char* convertedPixels = new unsigned char[texture->getWidth() * texture->getHeight() * sizeof(uint32)];
|
||||||
|
|
||||||
for (mipLevel = 0; mipLevel < source.getMipCount(); ++mipLevel) {
|
for (mipLevel = 0; mipLevel < output.getMipCount(); ++mipLevel) {
|
||||||
auto mipDims = texture->evalMipDimensions(mipLevel);
|
auto mipDims = texture->evalMipDimensions(mipLevel);
|
||||||
auto mipSize = texture->evalMipFaceSize(mipLevel);
|
auto mipSize = texture->evalMipFaceSize(mipLevel);
|
||||||
auto& mip = source.getMip(mipLevel);
|
auto& mip = output.getMip(mipLevel);
|
||||||
|
|
||||||
for (face = 0; face < 6; face++) {
|
for (face = 0; face < 6; face++) {
|
||||||
convertFromFloat(convertedPixels, mipDims.x, mipDims.y, sizeof(uint32)*mipDims.x, textureFormat, mip[face]);
|
convertFromFloat(convertedPixels, mipDims.x, mipDims.y, sizeof(uint32)*mipDims.x, textureFormat, mip[face]);
|
||||||
|
@ -1620,7 +1615,7 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options & CUBE_GGX_CONVOLVE) {
|
if (options & CUBE_GGX_CONVOLVE) {
|
||||||
convolveWithGGX(theTexture.get(), target, abortProcessing);
|
convolveForGGX(theTexture.get(), target, abortProcessing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
#include "ViewFrustum.h"
|
#include "ViewFrustum.h"
|
||||||
#include "GeometryCache.h"
|
#include "GeometryCache.h"
|
||||||
#include "FramebufferCache.h"
|
#include "FramebufferCache.h"
|
||||||
|
#include "RandomAndNoise.h"
|
||||||
|
|
||||||
namespace ru {
|
namespace ru {
|
||||||
using render_utils::slot::texture::Texture;
|
using render_utils::slot::texture::Texture;
|
||||||
|
@ -359,36 +359,11 @@ int JitterSampleConfig::play() {
|
||||||
return _state;
|
return _state;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <int B>
|
|
||||||
class Halton {
|
|
||||||
public:
|
|
||||||
|
|
||||||
float eval(int index) const {
|
|
||||||
float f = 1.0f;
|
|
||||||
float r = 0.0f;
|
|
||||||
float invB = 1.0f / (float)B;
|
|
||||||
index++; // Indices start at 1, not 0
|
|
||||||
|
|
||||||
while (index > 0) {
|
|
||||||
f = f * invB;
|
|
||||||
r = r + f * (float)(index % B);
|
|
||||||
index = index / B;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
JitterSample::SampleSequence::SampleSequence(){
|
JitterSample::SampleSequence::SampleSequence(){
|
||||||
// Halton sequence (2,3)
|
// Halton sequence (2,3)
|
||||||
Halton<2> genX;
|
|
||||||
Halton<3> genY;
|
|
||||||
|
|
||||||
for (int i = 0; i < SEQUENCE_LENGTH; i++) {
|
for (int i = 0; i < SEQUENCE_LENGTH; i++) {
|
||||||
offsets[i] = glm::vec2(genX.eval(i), genY.eval(i));
|
offsets[i] = glm::vec2(evaluateHalton<2>(i), evaluateHalton<3>(i));
|
||||||
offsets[i] -= vec2(0.5f);
|
offsets[i] -= vec2(0.5f);
|
||||||
}
|
}
|
||||||
offsets[SEQUENCE_LENGTH] = glm::vec2(0.0f);
|
offsets[SEQUENCE_LENGTH] = glm::vec2(0.0f);
|
||||||
|
|
47
libraries/shared/src/RandomAndNoise.h
Normal file
47
libraries/shared/src/RandomAndNoise.h
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// RandomAndNoise.h
|
||||||
|
//
|
||||||
|
// Created by Olivier Prat on 05/16/18.
|
||||||
|
// Copyright 2018 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
#ifndef RANDOM_AND_NOISE_H
|
||||||
|
#define RANDOM_AND_NOISE_H
|
||||||
|
|
||||||
|
#include <glm/vec2.hpp>
|
||||||
|
|
||||||
|
// Low discrepancy Halton sequence generator
|
||||||
|
template <int B>
|
||||||
|
float evaluateHalton(int index) {
|
||||||
|
float f = 1.0f;
|
||||||
|
float r = 0.0f;
|
||||||
|
float invB = 1.0f / (float)B;
|
||||||
|
index++; // Indices start at 1, not 0
|
||||||
|
|
||||||
|
while (index > 0) {
|
||||||
|
f = f * invB;
|
||||||
|
r = r + f * (float)(index % B);
|
||||||
|
index = index / B;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float getRadicalInverseVdC(uint32_t bits) {
|
||||||
|
bits = (bits << 16u) | (bits >> 16u);
|
||||||
|
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
|
||||||
|
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
|
||||||
|
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
|
||||||
|
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
|
||||||
|
return float(bits) * 2.3283064365386963e-10f; // / 0x100000000\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Low discrepancy Hammersley 2D sequence generator
|
||||||
|
inline glm::vec2 evaluateHammersley(int k, const int sequenceLength) {
|
||||||
|
return glm::vec2(float(k) / float(sequenceLength), getRadicalInverseVdC(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in a new issue