Commit bc1c11bc authored by Christopher Tenter's avatar Christopher Tenter
Browse files

MSAA support

git-svn-id: http://www.openflipper.org/svnrepo/OpenFlipper/branches/Free@12825 383ad7c9-94d9-4d36-a494-682f7c89f535
parent 377403f8
......@@ -117,7 +117,7 @@ void SSAOPlugin::supportedDrawModes(ACG::SceneGraph::DrawModes::DrawMode& _mode)
//////////////////////////////////////////////////////////////////////////
void SSAOPlugin::reloadResources(int _viewerId)
void SSAOPlugin::reloadResources(int _viewerId, unsigned int _sceneTexWidth, unsigned int _sceneTexHeight)
{
ViewerResources* p = &viewerRes_[_viewerId];
......@@ -136,6 +136,9 @@ void SSAOPlugin::reloadResources(int _viewerId)
p->rtDownWidth_ = p->rtWidth_ / 2;
p->rtDownHeight_ = p->rtHeight_ / 2;
p->rtSceneWidth_ = _sceneTexWidth;
p->rtSceneHeight_ = _sceneTexHeight;
// the scene texture contains the color result of a standard scene render pass
// format: R8G8B8A8
glGenTextures(1, &p->sceneBufTex_);
......@@ -144,9 +147,9 @@ void SSAOPlugin::reloadResources(int _viewerId)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// filter: none (1 to 1 mapping in final pass)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, p->glWidth_, p->glHeight_, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, p->rtSceneWidth_, p->rtSceneHeight_, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
// depth buf render texture
// format: R32F, maybe change to R16F if it works ok
......@@ -202,27 +205,49 @@ void SSAOPlugin::reloadResources(int _viewerId)
ACG::glCheckErrors();
// create depth render buffer
glGenRenderbuffersEXT(1, &p->depthRenderBuf_);
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, p->depthRenderBuf_);
glGenRenderbuffersEXT(1, &p->depthSSAORenderBuf_);
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, p->depthSSAORenderBuf_);
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, p->glWidth_, p->glHeight_);
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
// initialize the fbo
glGenFramebuffersEXT(1, &p->sceneFbo_);
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, p->sceneFbo_);
glGenFramebuffersEXT(1, &p->ssaoFbo_);
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, p->ssaoFbo_);
// color_attachment order:
// scene color, depth, scene normals, occlusion
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, p->sceneBufTex_, 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, p->depthBufTex_, 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT2_EXT, GL_TEXTURE_2D, p->sceneNormalTex_, 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT3_EXT, GL_TEXTURE_2D, p->occlusionTex_, 0);
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, p->depthRenderBuf_);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, p->depthBufTex_, 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, p->sceneNormalTex_, 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT2_EXT, GL_TEXTURE_2D, p->occlusionTex_, 0);
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, p->depthSSAORenderBuf_);
GLenum fboStatus = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
if(fboStatus != GL_FRAMEBUFFER_COMPLETE_EXT)
printf("SSAO Plugin: ssaoFbo failed to initialize\n");
glGenFramebuffersEXT(1, &p->sceneFbo_);
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, p->sceneFbo_);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, p->sceneBufTex_, 0);
if (p->rtSceneWidth_ > p->rtWidth_ || p->rtSceneHeight_ > p->rtHeight_)
{
// use new depth buffer for multisampling
glGenRenderbuffersEXT(1, &p->depthSceneRenderBuf_);
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, p->depthSceneRenderBuf_);
glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, p->rtSceneWidth_, p->rtSceneHeight_);
glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, p->depthSceneRenderBuf_);
}
else
glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, p->depthSSAORenderBuf_);
fboStatus = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
if(fboStatus != GL_FRAMEBUFFER_COMPLETE_EXT)
printf("SSAO Plugin: sceneFbo failed to initialize\n");
glGenFramebuffersEXT(1, &p->blurFbo_);
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, p->blurFbo_);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, p->downsampledTex_, 0);
......@@ -245,9 +270,11 @@ void SSAOPlugin::reloadResources(int _viewerId)
"SSAO/downsampling_fragment.glsl",
"SSAO/blur_fragment.glsl",
"SSAO/ssao_fragment.glsl",
"SSAO/final_fragment.glsl"};
"SSAO/final_fragment.glsl",
"SSAO/final_MSAA_vertex.glsl",
"SSAO/final_MSAA_fragment.glsl"};
for (int i = 0; i < 7; ++i)
for (int i = 0; i < 9; ++i)
{
QString shaderFile = OpenFlipper::Options::shaderDirStr() + QDir::separator() + QString(ShaderFiles[i]);
......@@ -257,19 +284,20 @@ void SSAOPlugin::reloadResources(int _viewerId)
if (shaders_[i]) continue;
#endif
if (i < 2) // first two are vertex shaders
if (i < 2 || i == 7) // first two are vertex shaders
shaders_[i] = GLSL::loadVertexShader(shaderFile.toUtf8());
else
shaders_[i] = GLSL::loadFragmentShader(shaderFile.toUtf8());
if (!shaders_[i]) {
if (!shaders_[i])
{
log(LOGERR, QString(ShaderFiles[i]) + QString(" could not be loaded and compiled"));
return;
}
}
// all needed glprograms
for (int i = 0; i < 5; ++i)
for (int i = 0; i < 6; ++i)
{
#ifndef SSAO_SHADER_DEBUG_MODE
if (!programs_[i])
......@@ -300,6 +328,10 @@ void SSAOPlugin::reloadResources(int _viewerId)
case PROG_FINAL:
pr->attach(shaders_[1]);
pr->attach(shaders_[6]); break;
case PROG_FINAL_MSAA:
pr->attach(shaders_[7]);
pr->attach(shaders_[8]); break;
}
pr->link();
......@@ -332,11 +364,7 @@ void SSAOPlugin::reloadResources(int _viewerId)
randVecs[i][2] = sinf(theta);
randVecs[i][3] = cosf(theta);
}
#ifdef ARCH_DARWIN
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F_ARB, 4, 4, 0, GL_RGBA, GL_FLOAT, randVecs);
#else
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 4, 4, 0, GL_RGBA, GL_FLOAT, randVecs);
#endif
}
ACG::GLState::bindTexture(GL_TEXTURE_2D, 0);
......@@ -375,9 +403,12 @@ void SSAOPlugin::destroyResources(int _viewerId)
ViewerResources* p = &viewerRes_[_viewerId];
if (p->sceneFbo_) glDeleteFramebuffersEXT(1, &p->sceneFbo_);
if (p->ssaoFbo_) glDeleteFramebuffersEXT(1, &p->ssaoFbo_);
if (p->blurFbo_) glDeleteFramebuffersEXT(1, &p->blurFbo_);
if (p->depthRenderBuf_) glDeleteRenderbuffersEXT(1, &p->depthRenderBuf_);
if (p->depthSSAORenderBuf_) glDeleteRenderbuffersEXT(1, &p->depthSSAORenderBuf_);
if (p->depthSceneRenderBuf_) glDeleteRenderbuffersEXT(1, &p->depthSceneRenderBuf_);
if (p->sceneBufTex_) glDeleteTextures(1, &p->sceneBufTex_);
if (p->depthBufTex_) glDeleteTextures(1, &p->depthBufTex_);
......@@ -462,12 +493,16 @@ void SSAOPlugin::render(ACG::GLState* _glState, Viewer::ViewerProperties& _prope
pViewer->glWidth_ = _glState->viewport_width();
pViewer->glHeight_ = _glState->viewport_height();
if (pViewer->glWidth_ != pViewer->rtWidth_ || pViewer->glHeight_ != pViewer->rtHeight_)
reloadResources(viewerId);
if (_properties.multisampling())
{
if ((pViewer->glWidth_ * 2 != pViewer->rtSceneWidth_) || (pViewer->glHeight_ * 2 != pViewer->rtSceneHeight_))
reloadResources(viewerId, pViewer->glWidth_ * 2, pViewer->glHeight_ * 2);
}
else if ((pViewer->glWidth_ != pViewer->rtSceneWidth_) || (pViewer->glHeight_ != pViewer->rtSceneHeight_))
reloadResources(viewerId, pViewer->glWidth_, pViewer->glHeight_);
BaseNode* sceneGraphRoot = PluginFunctions::getSceneGraphRootNode();
GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0_EXT,
GL_COLOR_ATTACHMENT1_EXT,
GL_COLOR_ATTACHMENT2_EXT,
......@@ -481,57 +516,32 @@ void SSAOPlugin::render(ACG::GLState* _glState, Viewer::ViewerProperties& _prope
GLint oldViewport[4];
glGetIntegerv(GL_VIEWPORT, oldViewport);
glViewport(0, 0, pViewer->glWidth_, pViewer->glHeight_);
for (int i = 0; i < 6; ++i)
{
ACG::GLState::activeTexture(GL_TEXTURE0 + i);
#ifdef ARCH_DARWIN
ACG::GLState::bindTexture(GL_TEXTURE_RECTANGLE_EXT, 0);
#else
ACG::GLState::bindTexture(GL_TEXTURE_RECTANGLE, 0);
#endif
ACG::GLState::bindTexture(GL_TEXTURE_2D, 0);
}
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, pViewer->sceneFbo_);
float texelSize[4] = {1.0f / float(pViewer->rtWidth_), 1.0f / float(pViewer->rtHeight_), 0.0f, 0.0f};
// ---------------------------------------------
// 1. init depth and normal targets
ACG::GLState::enable(GL_DEPTH_TEST);
ACG::GLState::depthFunc(GL_LESS);
glDepthMask(GL_TRUE);
// color attachment 1 and 2 stores the scene depth and normals
// clear first
ACG::GLState::drawBuffer(drawBuffers[1]);
glClearColor(maxDepth, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ACG::GLState::drawBuffer(drawBuffers[2]);
glClearColor(0.5f, 0.5f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
ACG::GLState::drawBuffers(2, drawBuffers+1);
programs_[PROG_INIT]->use();
drawScenePass(_glState, _properties, sceneGraphRoot);
programs_[PROG_INIT]->disable();
// ---------------------------------------------
// 2. render scene with standard materials:
// 1. render scene with standard materials:
glViewport(0, 0, pViewer->rtSceneWidth_, pViewer->rtSceneHeight_);
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, pViewer->sceneFbo_);
ACG::GLState::drawBuffer(drawBuffers[0]); // scene buffer in render target 0
glClearColor(_glState->clear_color()[0], _glState->clear_color()[1], _glState->clear_color()[2], 0);
// NOTE: for some reason the early z pass optimization does not work here
// using the depth buffer from previous pass gives z fighting
// early z cull optimization settings:
// ACG::GLState::enable(GL_DEPTH_TEST);
// ACG::GLState::depthFunc(GL_LEQUAL);
// ACG::GLState::lockDepthFunc();
// glDepthMask(GL_FALSE); // disable z writing
// glClear(GL_COLOR_BUFFER_BIT);
// ACG::GLState::enable(GL_DEPTH_TEST);
// ACG::GLState::depthFunc(GL_LEQUAL);
// ACG::GLState::lockDepthFunc();
// glDepthMask(GL_FALSE); // disable z writing
// glClear(GL_COLOR_BUFFER_BIT);
// without early z cull:
ACG::GLState::enable(GL_DEPTH_TEST);
......@@ -543,9 +553,36 @@ void SSAOPlugin::render(ACG::GLState* _glState, Viewer::ViewerProperties& _prope
ACG::GLState::unlockDepthFunc(); // unlock less-equal depth function
if (pViewer->rtSceneWidth_ != pViewer->glWidth_ || pViewer->rtSceneHeight_ != pViewer->glHeight_)
glViewport(0, 0, pViewer->glWidth_, pViewer->glHeight_);
// ---------------------------------------------
// 2. init depth and normal targets
ACG::GLState::bindFramebuffer(GL_FRAMEBUFFER_EXT, pViewer->ssaoFbo_);
ACG::GLState::enable(GL_DEPTH_TEST);
ACG::GLState::depthFunc(GL_LESS);
glDepthMask(GL_TRUE);
// color attachment 0 and 1 stores the scene depth and normals
// clear first
ACG::GLState::drawBuffer(drawBuffers[0]);
glClearColor(maxDepth, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ACG::GLState::drawBuffer(drawBuffers[1]);
glClearColor(0.5f, 0.5f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
ACG::GLState::drawBuffers(2, drawBuffers);
programs_[PROG_INIT]->use();
drawScenePass(_glState, _properties, sceneGraphRoot);
programs_[PROG_INIT]->disable();
// ---------------------------------------------
// 3. compute occlusion
ACG::GLState::drawBuffer(drawBuffers[3]); // occlusion buffer in render target 3
ACG::GLState::drawBuffer(drawBuffers[2]); // occlusion buffer in render target 2
ACG::GLState::disable(GL_DEPTH_TEST);
texelSize[0] = 1.0f / float(pViewer->rtWidth_);
......@@ -606,6 +643,7 @@ void SSAOPlugin::render(ACG::GLState* _glState, Viewer::ViewerProperties& _prope
programs_[PROG_BLUR]->use();
programs_[PROG_BLUR]->setUniform("DepthTex", 1);
programs_[PROG_BLUR]->setUniform("EdgeBlur", _properties.multisampling() ? 0.3f : 0.0f);
ACG::GLState::activeTexture(GL_TEXTURE1);
ACG::GLState::bindTexture(GL_TEXTURE_2D, pViewer->depthBufTex_);
......@@ -633,13 +671,17 @@ void SSAOPlugin::render(ACG::GLState* _glState, Viewer::ViewerProperties& _prope
ACG::GLState::activeTexture(GL_TEXTURE0);
ACG::GLState::bindTexture(GL_TEXTURE_2D, pViewer->sceneBufTex_);
programs_[PROG_FINAL]->use();
programs_[PROG_FINAL]->setUniform("OcclusionTex", 1);
programs_[PROG_FINAL]->setUniform("SceneTex", 0);
GLSL::Program* finalProg = programs_[_properties.multisampling() ? PROG_FINAL_MSAA : PROG_FINAL];
finalProg->use();
finalProg->setUniform("OcclusionTex", 1);
finalProg->setUniform("SceneTex", 0);
if (_properties.multisampling())
finalProg->setUniform("SceneTexelSize", ACG::Vec2f(1.0f / float(pViewer->rtSceneWidth_), 1.0f / float(pViewer->rtSceneHeight_)));
drawQuadProj();
programs_[PROG_FINAL]->disable();
finalProg->disable();
//-----------------------------------------
......
......@@ -91,7 +91,7 @@ private:
void destroyResources(int _viewerId);
/// reload gl resources
void reloadResources(int _viewerId);
void reloadResources(int _viewerId, unsigned int _sceneTexWidth, unsigned int _sceneTexHeight);
/// draw a quad in projection space (only positions)
void drawQuadProj(float _x0 = -1.0f, float _y0 = 1.0f,
......@@ -126,6 +126,11 @@ private:
/// viewer window height
unsigned int glHeight_;
/// scene render target width
unsigned int rtSceneWidth_;
/// scene render target height
unsigned int rtSceneHeight_;
/// render target width
unsigned int rtWidth_;
/// render target height
......@@ -143,8 +148,11 @@ private:
/// R8G8B8 format
GLuint sceneNormalTex_;
/// depth renderbuffer for early Z
GLuint depthRenderBuf_;
/// depth renderbuffer for ssaoFbo
GLuint depthSSAORenderBuf_;
/// depth renderbuffer for sceneFbo
GLuint depthSceneRenderBuf_;
/// standard scene without a render target
GLuint sceneBufTex_;
......@@ -158,19 +166,23 @@ private:
/// occlusion render target
GLuint occlusionTex_;
/// sceneFbo for scene rendering and early z pass
/// attachement order: scene, depth, normal, occlusion
/// sceneFbo for scene color rendering only (seperated for multisampling)
/// attachment order: only scene color texture
GLuint sceneFbo_;
/// ssaoFbo for deferred rendering
/// attachment order: depth, normal, occlusion
GLuint ssaoFbo_;
/// blurFbo for downsampling and gaussian blur filter
/// attachement order: downsampled, downsampledTmp, occlusion
/// attachment order: downsampled, downsampledTmp, occlusion
GLuint blurFbo_;
};
std::map<int, ViewerResources> viewerRes_;
/// shader resources
GLSL::Shader* shaders_[8];
GLSL::Shader* shaders_[10];
enum
{
......@@ -178,11 +190,12 @@ private:
PROG_DOWNSAMPLING,
PROG_BLUR,
PROG_SSAO,
PROG_FINAL
PROG_FINAL,
PROG_FINAL_MSAA
};
/// shader programs
GLSL::Program* programs_[5];
GLSL::Program* programs_[6];
/// random vector table for sample offset rotation
GLuint randomVecTex_;
......
......@@ -15,11 +15,13 @@ uniform float Kernel[5]; // gaussian distribution in one direction only
const float DepthThreshold = 0.01;
uniform float EdgeBlur; // [0,1]: 0 -> no edge blurring, 1 -> blur everywhere
float GetDepthWeight(vec2 uv, float Depth)
{
float SampleDepth = texture2D(DepthTex, uv).x;
float DepthRange = abs(Depth - SampleDepth) / (Depth + 0.001);
float DepthCheck = DepthRange > DepthThreshold ? 0.0 : 1.0;
float DepthCheck = DepthRange > DepthThreshold ? EdgeBlur : 1.0;
return DepthCheck;
}
......
// multiplies scene with occlusion value
// render into back buffer
varying vec2 vTexCoord0;
varying vec4 vTexCoord1;
varying vec4 vTexCoord2;
varying vec4 vTexCoord3;
varying vec4 vTexCoord4;
uniform sampler2D SceneTex;
uniform sampler2D OcclusionTex;
void main(void)
{
vec4 Center = texture2D(SceneTex, vTexCoord0);
// direct neighbors
vec4 DirectN = texture2D(SceneTex, vTexCoord1.xy);
DirectN += texture2D(SceneTex, vTexCoord1.zw);
DirectN += texture2D(SceneTex, vTexCoord2.xy);
DirectN += texture2D(SceneTex, vTexCoord2.zw);
// corner neighbors
vec4 CornerN = texture2D(SceneTex, vTexCoord2.xy);
CornerN += texture2D(SceneTex, vTexCoord2.zw);
CornerN += texture2D(SceneTex, vTexCoord3.xy);
CornerN += texture2D(SceneTex, vTexCoord3.zw);
// using the corner neighbors results in a ~90 instruction shader on HD 5000 series
// vec4 Result = (Center * 0.5 + DirectN * (0.5 * 0.25))* 0.75 + CornerN * (0.25 * 0.25);
// direct neighbors gives a ~60 instruction shader with neglectable quality loss
vec4 Result = (Center * 0.5 + DirectN * (0.5 * 0.25));
float Occlusion = clamp(texture2D(OcclusionTex, vTexCoord0).x, 0.0, 1.0);
Result.rgb *= Occlusion;
gl_FragData[0] = Result;
}
\ No newline at end of file
varying vec2 vTexCoord0;
varying vec4 vTexCoord1;
varying vec4 vTexCoord2;
varying vec4 vTexCoord3;
varying vec4 vTexCoord4;
uniform vec2 SceneTexelSize;
void main(void)
{
gl_Position = gl_Vertex;
vec2 uv = gl_Vertex.xy * vec2(0.5, 0.5) + vec2(0.5, 0.5);
float sc = 1.2215; // use some texture filtering
vec2 d = SceneTexelSize * sc;
vTexCoord0 = uv;
vTexCoord1 = vec4(uv + vec2(d.x, 0), uv - vec2(d.x, 0));
vTexCoord2 = vec4(uv + vec2(0, d.y), uv - vec2(0, d.y));
vTexCoord3 = vec4(uv + d, uv - d);
vTexCoord4 = vec4(uv + vec2(d.x, -d.y), uv - vec2(d.x, -d.y));
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment