Notice to GitKraken users: A vulnerability has been found in the SSH key generation of GitKraken versions 7.6.0 to 8.0.0 (https://www.gitkraken.com/blog/weak-ssh-key-fix). If you use GitKraken and have generated a SSH key using one of these versions, please remove it both from your local workstation and from your LRZ GitLab profile.

21.10.2021, 9:00 - 11:00: Due to updates GitLab may be unavailable for some minutes between 09:00 and 11:00.

Commit 91039094 authored by schultezub's avatar schultezub
Browse files

further work on Virtual Mirror implementation

git-svn-id: https://camplinux.in.tum.de/svn/campvis/trunk@289 bb408c1c-ae56-11e1-83d9-df6b3e0c105e
parent 665dcf97
......@@ -49,7 +49,7 @@ namespace TUMVis {
, _exitImageID("exitImageID", "Input Exit Points Image", "", DataNameProperty::READ)
, _camera("camera", "Camera")
, _transferFunction("transferFunction", "Transfer Function", new SimpleTransferFunction(256))
, _samplingStepSize("samplingStepSize", "Sampling Step Size", .1f, 0.001f, 1.f)
, _samplingStepSize("samplingStepSize", "Sampling Step Size", .02f, 0.001f, 1.f)
, _jitterEntryPoints("jitterEntryPoints", "Jitter Entry Points", true)
, _jitterStepSizeMultiplier("jitterStepSizeMultiplier", "Jitter Step Size Multiplier", .5f, .1f, 1.f)
, _fragmentShaderFilename(fragmentShaderFileName)
......@@ -96,6 +96,7 @@ namespace TUMVis {
}
glPushAttrib(GL_ALL_ATTRIB_BITS);
glEnable(GL_DEPTH_TEST);
_shader->activate();
_shader->setAttributeLocation(0, "in_Position");
......
......@@ -44,9 +44,10 @@ namespace TUMVis {
, _vmRenderer(_renderTargetSize)
, _eepGenerator(_renderTargetSize)
, _vmEepGenerator(_renderTargetSize)
, _drrraycater(_renderTargetSize)
, _simpleRaycaster(_renderTargetSize)
, _dvrNormal(_renderTargetSize)
, _dvrVM(_renderTargetSize)
, _clRaycaster(_renderTargetSize)
, _combine(_renderTargetSize)
, _trackballEH(0)
{
addProperty(&_camera);
......@@ -60,9 +61,10 @@ namespace TUMVis {
_processors.push_back(&_vmRenderer);
_processors.push_back(&_eepGenerator);
_processors.push_back(&_vmEepGenerator);
_processors.push_back(&_drrraycater);
_processors.push_back(&_simpleRaycaster);
_processors.push_back(&_dvrNormal);
_processors.push_back(&_dvrVM);
_processors.push_back(&_clRaycaster);
_processors.push_back(&_combine);
}
DVRVis::~DVRVis() {
......@@ -76,18 +78,18 @@ namespace TUMVis {
_camera.addSharedProperty(&_vmRenderer._camera);
_camera.addSharedProperty(&_eepGenerator._camera);
_camera.addSharedProperty(&_vmEepGenerator._camera);
_camera.addSharedProperty(&_drrraycater._camera);
_camera.addSharedProperty(&_simpleRaycaster._camera);
_camera.addSharedProperty(&_dvrNormal._camera);
_camera.addSharedProperty(&_dvrVM._camera);
_camera.addSharedProperty(&_clRaycaster._camera);
_imageReader._url.setValue("D:\\Medical Data\\smallHeart.mhd");
_imageReader._targetImageID.setValue("reader.output");
_drrraycater._targetImageID.setValue("drr.output");
_drrraycater._sourceImageID.setValue("eep.input");
_dvrNormal._targetImageID.setValue("drr.output");
_dvrNormal._sourceImageID.setValue("eep.input");
_simpleRaycaster._targetImageID.setValue("dvr.output");
_simpleRaycaster._sourceImageID.setValue("eep.input");
_dvrVM._targetImageID.setValue("dvr.output");
_dvrVM._sourceImageID.setValue("eep.input");
_clRaycaster._targetImageID.setValue("clr.output");
_clRaycaster._sourceImageID.setValue("clr.input");
......@@ -102,7 +104,7 @@ namespace TUMVis {
_vmEepGenerator._applyMask.setValue(true);
_vmEepGenerator._enableMirror.setValue(true);
_renderTargetID.setValue("eep.entry");
_renderTargetID.setValue("combine");
_pgGenerator._geometryID.connect(&_vmEepGenerator._geometryID);
_vmgGenerator._mirrorID.connect(&_vmEepGenerator._mirrorID);
......@@ -111,23 +113,28 @@ namespace TUMVis {
_vmgGenerator._poi.setValue(tgt::vec3(40.f, 40.f, 40.f));
_vmgGenerator._size.setValue(60.f);
_eepGenerator._entryImageID.connect(&_drrraycater._entryImageID);
_eepGenerator._entryImageID.connect(&_simpleRaycaster._entryImageID);
_eepGenerator._entryImageID.connect(&_dvrNormal._entryImageID);
_vmEepGenerator._entryImageID.connect(&_dvrVM._entryImageID);
_eepGenerator._entryImageID.connect(&_clRaycaster._entryImageID);
_eepGenerator._exitImageID.connect(&_drrraycater._exitImageID);
_eepGenerator._exitImageID.connect(&_simpleRaycaster._exitImageID);
_eepGenerator._exitImageID.connect(&_dvrNormal._exitImageID);
_vmEepGenerator._exitImageID.connect(&_dvrVM._exitImageID);
_eepGenerator._exitImageID.connect(&_clRaycaster._exitImageID);
_dvrNormal._targetImageID.connect(&_combine._normalImageID);
_dvrVM._targetImageID.connect(&_combine._mirrorImageID);
_combine._targetImageID.setValue("combine");
_imageReader.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_vmgGenerator.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_vmRenderer.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_pgGenerator.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_eepGenerator.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_vmEepGenerator.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_drrraycater.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_simpleRaycaster.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_dvrNormal.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_dvrVM.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_clRaycaster.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_combine.s_invalidated.connect<DVRVis>(this, &DVRVis::onProcessorInvalidated);
_trackballEH->setViewportSize(_renderTargetSize.getValue());
_renderTargetSize.s_changed.connect<DVRVis>(this, &DVRVis::onRenderTargetSizeChanged);
......@@ -180,15 +187,18 @@ namespace TUMVis {
if (! _vmEepGenerator.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_vmEepGenerator);
}
if (!_drrraycater.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_drrraycater);
if (!_dvrNormal.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_dvrNormal);
}
if (!_simpleRaycaster.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_simpleRaycaster);
if (!_dvrVM.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_dvrVM);
}
if (!_clRaycaster.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_clRaycaster);
}
if (!_combine.getInvalidationLevel().isValid()) {
lockGLContextAndExecuteProcessor(&_combine);
}
}
const std::string DVRVis::getName() const {
......
......@@ -41,6 +41,7 @@
#include "modules/vis/drrraycaster.h"
#include "modules/vis/simpleraycaster.h"
#include "modules/vis/clraycaster.h"
#include "modules/vis/virtualmirrorcombine.h"
namespace TUMVis {
class DVRVis : public VisualizationPipeline {
......@@ -76,9 +77,11 @@ namespace TUMVis {
GeometryRenderer _vmRenderer;
EEPGenerator _eepGenerator;
EEPGenerator _vmEepGenerator;
DRRRaycaster _drrraycater;
SimpleRaycaster _simpleRaycaster;
SimpleRaycaster _dvrNormal;
SimpleRaycaster _dvrVM;
CLRaycaster _clRaycaster;
VirtualMirrorCombine _combine;
TrackballNavigationEventHandler* _trackballEH;
};
......
......@@ -36,7 +36,7 @@ namespace TUMVis {
DRRRaycaster::DRRRaycaster(GenericProperty<tgt::ivec2>& canvasSize)
: RaycastingProcessor(canvasSize, "modules/vis/drrraycaster.frag", false)
, _targetImageID("targetImageID", "Output Image", "")
, _targetImageID("targetImageID", "Output Image", "", DataNameProperty::WRITE)
, _shift("shift", "Normalization Shift", 0.f, -10.f, 10.f)
, _scale("scale", "Normalization Scale", 1.f, 0.f, 1000.f)
, _invertMapping("invertMapping", "Invert Mapping", false, InvalidationLevel::INVALID_RESULT | InvalidationLevel::INVALID_SHADER)
......@@ -68,6 +68,7 @@ namespace TUMVis {
rt->deactivate();
data.addData(_targetImageID.getValue(), rt);
_targetImageID.issueWrite();
}
std::string DRRRaycaster::generateHeader() const {
......
......@@ -63,7 +63,7 @@ namespace TUMVis {
/// \see AbstractProcessor::getDescription()
virtual const std::string getDescription() const { return "Creates a Digitally Reconstructed Radiograph."; };
GenericProperty<std::string> _targetImageID; ///< image ID for output image
DataNameProperty _targetImageID; ///< image ID for output image
FloatProperty _shift;
FloatProperty _scale;
......
......@@ -125,7 +125,7 @@ namespace TUMVis {
-2*n.x*n.z , -2*n.y*n.z , 1 - 2*n.z*n.z, 0,
2*n.x*k , 2*n.y*k , 2*n.z*k , 1));
// TODO: double check, whether matrix transpose is necessary, and if so, hardcode it
// TODO: double check, whether matrix transpose is necessary
}
else {
LERROR("No suitable virtual mirror geometry found.");
......
......@@ -88,6 +88,7 @@ namespace TUMVis {
ImageDataRenderTarget* rt = new ImageDataRenderTarget(tgt::svec3(_renderTargetSize.getValue(), 1), GL_RGBA16);
rt->activate();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glClearDepth(1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
......
......@@ -36,9 +36,12 @@ namespace TUMVis {
SimpleRaycaster::SimpleRaycaster(GenericProperty<tgt::ivec2>& canvasSize)
: RaycastingProcessor(canvasSize, "modules/vis/simpleraycaster.frag", true)
, _targetImageID("targetImageID", "Output Image", "")
, _targetImageID("targetImageID", "Output Image", "", DataNameProperty::WRITE)
{
addProperty(&_targetImageID);
// TODO: remove hack
_transferFunction.getTF()->setIntensityDomain(tgt::vec2(0.01f, 0.05f));
}
SimpleRaycaster::~SimpleRaycaster() {
......@@ -56,5 +59,6 @@ namespace TUMVis {
rt->deactivate();
data.addData(_targetImageID.getValue(), rt);
_targetImageID.issueWrite();
}
}
......@@ -78,7 +78,7 @@ vec4 performRaycasting(in vec3 entryPoint, in vec3 exitPoint, in vec2 texCoords)
if (color.a > 0.0) {
// accomodate for variable sampling rates (base interval defined by mod_compositing.frag)
color.a = 1.0 - pow(1.0 - color.a, _samplingStepSize * SAMPLING_BASE_INTERVAL_RCP);
result.rgb = result.rgb + (1.0 - result.a) * color.a * color.rgb;
result.rgb = mix(color.rgb, result.rgb, result.a);
result.a = result.a + (1.0 -result.a) * color.a;
}
......
......@@ -63,7 +63,7 @@ namespace TUMVis {
/// \see AbstractProcessor::getDescription()
virtual const std::string getDescription() const { return "Performs a simple volume ray casting."; };
GenericProperty<std::string> _targetImageID; ///< image ID for output image
DataNameProperty _targetImageID; ///< image ID for output image
protected:
/// \see RaycastingProcessor::processImpl()
......
// ================================================================================================
//
// This file is part of the TUMVis Visualization Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012, all rights reserved,
// Christian Schulte zu Berge (christian.szb@in.tum.de)
// Chair for Computer Aided Medical Procedures
// Technische Universitt Mnchen
// Boltzmannstr. 3, 85748 Garching b. Mnchen, Germany
//
// The licensing of this softare is not yet resolved. Until then, redistribution in source or
// binary forms outside the CAMP chair is not permitted, unless explicitly stated in legal form.
// However, the names of the original authors and the above copyright notice must retain in its
// original state in any case.
//
// Legal disclaimer provided by the BSD license:
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// ================================================================================================
#include "virtualmirrorcombine.h"
#include "tgt/logmanager.h"
#include "tgt/shadermanager.h"
#include "tgt/textureunit.h"
#include "core/datastructures/imagedata.h"
#include "core/datastructures/imagedatagl.h"
#include "core/datastructures/imagedatarendertarget.h"
#include "core/datastructures/imagedataconverter.h"
#include "core/classification/simpletransferfunction.h"
#include "core/tools/quadrenderer.h"
namespace TUMVis {
const std::string VirtualMirrorCombine::loggerCat_ = "TUMVis.modules.vis.VirtualMirrorCombine";
VirtualMirrorCombine::VirtualMirrorCombine(GenericProperty<tgt::ivec2>& canvasSize)
: VisualizationProcessor(canvasSize)
, _normalImageID("normalImageID", "Normal DVR Input Image", "", DataNameProperty::READ)
, _mirrorImageID("mirrorImageIde", "Mirror DVR Input Image", "", DataNameProperty::READ)
, _targetImageID("targetImageID", "Output Image", "", DataNameProperty::WRITE)
, _shader(0)
{
addProperty(&_normalImageID);
addProperty(&_mirrorImageID);
addProperty(&_targetImageID);
}
VirtualMirrorCombine::~VirtualMirrorCombine() {
}
void VirtualMirrorCombine::init() {
VisualizationProcessor::init();
_shader = ShdrMgr.loadSeparate("core/glsl/passthrough.vert", "modules/vis/virtualmirrorcombine.frag", "", false);
_shader->setAttributeLocation(0, "in_Position");
_shader->setAttributeLocation(1, "in_TexCoord");
}
void VirtualMirrorCombine::deinit() {
VisualizationProcessor::deinit();
ShdrMgr.dispose(_shader);
}
void VirtualMirrorCombine::process(DataContainer& data) {
DataContainer::ScopedTypedData<ImageDataRenderTarget> normalImage(data, _normalImageID.getValue());
DataContainer::ScopedTypedData<ImageDataRenderTarget> mirrorImage(data, _mirrorImageID.getValue());
if (normalImage != 0 && mirrorImage != 0) {
ImageDataRenderTarget* rt = new ImageDataRenderTarget(tgt::svec3(_renderTargetSize.getValue(), 1));
glPushAttrib(GL_ALL_ATTRIB_BITS);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);
_shader->activate();
tgt::TextureUnit normalColorUnit, normalDepthUnit, mirrorColorUnit, mirrorDepthUnit;
normalImage->bind(_shader, &normalColorUnit, &normalDepthUnit, "_normalColor", "_normalDepth");
mirrorImage->bind(_shader, &mirrorColorUnit, &mirrorDepthUnit, "_mirrorColor", "_mirrorDepth");
rt->activate();
LGL_ERROR;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
QuadRdr.renderQuad();
rt->deactivate();
_shader->deactivate();
tgt::TextureUnit::setZeroUnit();
glPopAttrib();
LGL_ERROR;
data.addData(_targetImageID.getValue(), rt);
_targetImageID.issueWrite();
}
else {
LERROR("No suitable input images found.");
}
_invalidationLevel.setValid();
}
}
// ================================================================================================
//
// This file is part of the TUMVis Visualization Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012, all rights reserved,
// Christian Schulte zu Berge (christian.szb@in.tum.de)
// Chair for Computer Aided Medical Procedures
// Technische Universität München
// Boltzmannstr. 3, 85748 Garching b. München, Germany
//
// The licensing of this softare is not yet resolved. Until then, redistribution in source or
// binary forms outside the CAMP chair is not permitted, unless explicitly stated in legal form.
// However, the names of the original authors and the above copyright notice must retain in its
// original state in any case.
//
// Legal disclaimer provided by the BSD license:
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// ================================================================================================
#version 330
in vec3 ex_TexCoord;
out vec4 out_Color;
#include "tools/texture2d.frag"
uniform Texture2D _normalColor;
uniform Texture2D _normalDepth;
uniform Texture2D _mirrorColor;
uniform Texture2D _mirrorDepth;
void main() {
float normalDepth = getElement2DNormalized(_normalDepth, ex_TexCoord.xy).z;
float mirrorDepth = getElement2DNormalized(_mirrorDepth, ex_TexCoord.xy).z;
if (normalDepth <= mirrorDepth) {
out_Color = getElement2DNormalized(_normalColor, ex_TexCoord.xy);
gl_FragDepth = normalDepth;
}
else {
out_Color = getElement2DNormalized(_mirrorColor, ex_TexCoord.xy);
gl_FragDepth = mirrorDepth;
}
}
// ================================================================================================
//
// This file is part of the TUMVis Visualization Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012, all rights reserved,
// Christian Schulte zu Berge (christian.szb@in.tum.de)
// Chair for Computer Aided Medical Procedures
// Technische Universitt Mnchen
// Boltzmannstr. 3, 85748 Garching b. Mnchen, Germany
//
// The licensing of this softare is not yet resolved. Until then, redistribution in source or
// binary forms outside the CAMP chair is not permitted, unless explicitly stated in legal form.
// However, the names of the original authors and the above copyright notice must retain in its
// original state in any case.
//
// Legal disclaimer provided by the BSD license:
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// ================================================================================================
#ifndef VIRTUALMIRRORCOMBINE_H__
#define VIRTUALMIRRORCOMBINE_H__
#include <string>
#include "core/pipeline/visualizationprocessor.h"
#include "core/properties/datanameproperty.h"
#include "core/properties/genericproperty.h"
#include "core/properties/numericproperty.h"
#include "core/properties/transferfunctionproperty.h"
namespace tgt {
class Shader;
}
namespace TUMVis {
class ImageData;
/**
* Extracts a slice from a 3D image and renders it into a rendertarget.
*/
class VirtualMirrorCombine : public VisualizationProcessor {
public:
/**
* Constructs a new VirtualMirrorCombine Processor
**/
VirtualMirrorCombine(GenericProperty<tgt::ivec2>& canvasSize);
/**
* Destructor
**/
virtual ~VirtualMirrorCombine();
/// \see AbstractProcessor::init
virtual void init();
/// \see AbstractProcessor::deinit
virtual void deinit();
/// \see AbstractProcessor::getName()
virtual const std::string getName() const { return "VirtualMirrorCombine"; };
/// \see AbstractProcessor::getDescription()
virtual const std::string getDescription() const { return "Combines Normal DVR and Virtual Mirror DVR images."; };
virtual void process(DataContainer& data);
DataNameProperty _normalImageID; ///< image ID for normal DVR input image
DataNameProperty _mirrorImageID; ///< image ID for mirror DVR input image
DataNameProperty _targetImageID; ///< image ID for output image
protected:
tgt::Shader* _shader; ///< Shader for slice rendering
static const std::string loggerCat_;
};
}
#endif // VIRTUALMIRRORCOMBINE_H__
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