1008 lines
38 KiB
C++
1008 lines
38 KiB
C++
/*
|
|
This file is part of Nori, a simple educational ray tracer
|
|
|
|
Copyright (c) 2015 by Wenzel Jakob
|
|
|
|
Nori is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License Version 3
|
|
as published by the Free Software Foundation.
|
|
|
|
Nori 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <nori/warp.h>
|
|
#include <nori/bsdf.h>
|
|
#include <nori/vector.h>
|
|
#include <nanogui/screen.h>
|
|
#include <nanogui/label.h>
|
|
#include <nanogui/window.h>
|
|
#include <nanogui/layout.h>
|
|
#include <nanogui/icons.h>
|
|
#include <nanogui/combobox.h>
|
|
#include <nanogui/slider.h>
|
|
#include <nanogui/textbox.h>
|
|
#include <nanogui/checkbox.h>
|
|
#include <nanogui/messagedialog.h>
|
|
#include <nanogui/renderpass.h>
|
|
#include <nanogui/shader.h>
|
|
#include <nanogui/texture.h>
|
|
#include <nanogui/screen.h>
|
|
#include <nanogui/opengl.h>
|
|
#include <nanogui/window.h>
|
|
|
|
#include <nanovg_gl.h>
|
|
|
|
#include <pcg32.h>
|
|
#include <hypothesis.h>
|
|
#include <tinyformat.h>
|
|
|
|
#include <Eigen/Geometry>
|
|
|
|
/* =======================================================================
|
|
* WARNING WARNING WARNING WARNING WARNING WARNING
|
|
* =======================================================================
|
|
* Remember to put on SAFETY GOGGLES before looking at this file. You
|
|
* are most certainly not expected to read or understand any of it.
|
|
* ======================================================================= */
|
|
|
|
#if defined(_MSC_VER)
|
|
# pragma warning (disable: 4305 4244)
|
|
#endif
|
|
|
|
using namespace nanogui;
|
|
using namespace std;
|
|
|
|
using nori::NoriException;
|
|
using nori::NoriObjectFactory;
|
|
using nori::Point2f;
|
|
using nori::Point2i;
|
|
using nori::Point3f;
|
|
using nori::Warp;
|
|
using nori::PropertyList;
|
|
using nori::BSDF;
|
|
using nori::BSDFQueryRecord;
|
|
using nori::Color3f;
|
|
|
|
|
|
enum PointType : int {
|
|
Independent = 0,
|
|
Grid,
|
|
Stratified
|
|
};
|
|
|
|
enum WarpType : int {
|
|
Square = 0,
|
|
Tent,
|
|
Disk,
|
|
UniformSphere,
|
|
UniformHemisphere,
|
|
CosineHemisphere,
|
|
Beckmann,
|
|
MicrofacetBRDF,
|
|
WarpTypeCount
|
|
};
|
|
|
|
static const std::string kWarpTypeNames[WarpTypeCount] = {
|
|
"square", "tent", "disk", "uniform_sphere", "uniform_hemisphere",
|
|
"cosine_hemisphere", "beckmann", "microfacet_brdf"
|
|
};
|
|
|
|
|
|
struct WarpTest {
|
|
static const int kDefaultXres = 51;
|
|
static const int kDefaultYres = 51;
|
|
|
|
WarpType warpType;
|
|
float parameterValue;
|
|
BSDF *bsdf;
|
|
BSDFQueryRecord bRec;
|
|
int xres, yres, res;
|
|
|
|
// Observed and expected frequencies, initialized after calling run().
|
|
std::unique_ptr<double[]> obsFrequencies, expFrequencies;
|
|
|
|
WarpTest(WarpType warpType_, float parameterValue_, BSDF *bsdf_ = nullptr,
|
|
BSDFQueryRecord bRec_ = BSDFQueryRecord(nori::Vector3f()),
|
|
int xres_ = kDefaultXres, int yres_ = kDefaultYres)
|
|
: warpType(warpType_), parameterValue(parameterValue_), bsdf(bsdf_),
|
|
bRec(bRec_), xres(xres_), yres(yres_) {
|
|
|
|
if (warpType != Square && warpType != Disk && warpType != Tent)
|
|
xres *= 2;
|
|
res = xres * yres;
|
|
}
|
|
|
|
std::pair<bool, std::string> run() {
|
|
int sampleCount = 1000 * res;
|
|
obsFrequencies.reset(new double[res]);
|
|
expFrequencies.reset(new double[res]);
|
|
memset(obsFrequencies.get(), 0, res*sizeof(double));
|
|
memset(expFrequencies.get(), 0, res*sizeof(double));
|
|
|
|
nori::MatrixXf points, values;
|
|
generatePoints(sampleCount, Independent, points, values);
|
|
|
|
for (int i=0; i<sampleCount; ++i) {
|
|
if (values(0, i) == 0)
|
|
continue;
|
|
nori::Vector3f sample = points.col(i);
|
|
float x, y;
|
|
|
|
if (warpType == Square) {
|
|
x = sample.x();
|
|
y = sample.y();
|
|
} else if (warpType == Disk || warpType == Tent) {
|
|
x = sample.x() * 0.5f + 0.5f;
|
|
y = sample.y() * 0.5f + 0.5f;
|
|
} else {
|
|
x = std::atan2(sample.y(), sample.x()) * INV_TWOPI;
|
|
if (x < 0)
|
|
x += 1;
|
|
y = sample.z() * 0.5f + 0.5f;
|
|
}
|
|
|
|
int xbin = std::min(xres-1, std::max(0, (int) std::floor(x * xres)));
|
|
int ybin = std::min(yres-1, std::max(0, (int) std::floor(y * yres)));
|
|
obsFrequencies[ybin * xres + xbin] += 1;
|
|
}
|
|
|
|
auto integrand = [&](double y, double x) -> double {
|
|
if (warpType == Square) {
|
|
return Warp::squareToUniformSquarePdf(Point2f(x, y));
|
|
} else if (warpType == Disk) {
|
|
x = x * 2 - 1; y = y * 2 - 1;
|
|
return Warp::squareToUniformDiskPdf(Point2f(x, y));
|
|
} else if (warpType == Tent) {
|
|
x = x * 2 - 1; y = y * 2 - 1;
|
|
return Warp::squareToTentPdf(Point2f(x, y));
|
|
} else {
|
|
x *= 2 * M_PI;
|
|
y = y * 2 - 1;
|
|
|
|
double sinTheta = std::sqrt(1 - y * y);
|
|
double sinPhi = std::sin(x),
|
|
cosPhi = std::cos(x);
|
|
|
|
nori::Vector3f v((float) (sinTheta * cosPhi),
|
|
(float) (sinTheta * sinPhi),
|
|
(float) y);
|
|
|
|
if (warpType == UniformSphere)
|
|
return Warp::squareToUniformSpherePdf(v);
|
|
else if (warpType == UniformHemisphere)
|
|
return Warp::squareToUniformHemispherePdf(v);
|
|
else if (warpType == CosineHemisphere)
|
|
return Warp::squareToCosineHemispherePdf(v);
|
|
else if (warpType == Beckmann)
|
|
return Warp::squareToBeckmannPdf(v, parameterValue);
|
|
else if (warpType == MicrofacetBRDF) {
|
|
BSDFQueryRecord br(bRec);
|
|
br.wo = v;
|
|
br.measure = nori::ESolidAngle;
|
|
return bsdf->pdf(br);
|
|
} else {
|
|
throw NoriException("Invalid warp type");
|
|
}
|
|
}
|
|
};
|
|
|
|
double scale = sampleCount;
|
|
if (warpType == Square)
|
|
scale *= 1;
|
|
else if (warpType == Disk || warpType == Tent)
|
|
scale *= 4;
|
|
else
|
|
scale *= 4*M_PI;
|
|
|
|
double *ptr = expFrequencies.get();
|
|
for (int y=0; y<yres; ++y) {
|
|
double yStart = y / (double) yres;
|
|
double yEnd = (y+1) / (double) yres;
|
|
for (int x=0; x<xres; ++x) {
|
|
double xStart = x / (double) xres;
|
|
double xEnd = (x+1) / (double) xres;
|
|
ptr[y * xres + x] = hypothesis::adaptiveSimpson2D(
|
|
integrand, yStart, xStart, yEnd, xEnd) * scale;
|
|
if (ptr[y * xres + x] < 0)
|
|
throw NoriException("The Pdf() function returned negative values!");
|
|
}
|
|
}
|
|
|
|
/* Write the test input data to disk for debugging */
|
|
hypothesis::chi2_dump(yres, xres, obsFrequencies.get(), expFrequencies.get(), "chitest.m");
|
|
|
|
/* Perform the Chi^2 test */
|
|
const int minExpFrequency = 5;
|
|
const float significanceLevel = 0.01f;
|
|
|
|
return hypothesis::chi2_test(yres*xres, obsFrequencies.get(),
|
|
expFrequencies.get(), sampleCount,
|
|
minExpFrequency, significanceLevel, 1);
|
|
}
|
|
|
|
|
|
std::pair<Point3f, float> warpPoint(const Point2f &sample) {
|
|
Point3f result;
|
|
|
|
switch (warpType) {
|
|
case Square:
|
|
result << Warp::squareToUniformSquare(sample), 0; break;
|
|
case Tent:
|
|
result << Warp::squareToTent(sample), 0; break;
|
|
case Disk:
|
|
result << Warp::squareToUniformDisk(sample), 0; break;
|
|
case UniformSphere:
|
|
result << Warp::squareToUniformSphere(sample); break;
|
|
case UniformHemisphere:
|
|
result << Warp::squareToUniformHemisphere(sample); break;
|
|
case CosineHemisphere:
|
|
result << Warp::squareToCosineHemisphere(sample); break;
|
|
case Beckmann:
|
|
result << Warp::squareToBeckmann(sample, parameterValue); break;
|
|
case MicrofacetBRDF: {
|
|
BSDFQueryRecord br(bRec);
|
|
float value = bsdf->sample(br, sample).getLuminance();
|
|
return std::make_pair(
|
|
br.wo,
|
|
value == 0 ? 0.f : bsdf->eval(br)[0]
|
|
);
|
|
}
|
|
default:
|
|
throw std::runtime_error("Unsupported warp type.");
|
|
}
|
|
|
|
return std::make_pair(result, 1.f);
|
|
}
|
|
|
|
|
|
void generatePoints(int &pointCount, PointType pointType,
|
|
nori::MatrixXf &positions, nori::MatrixXf &weights) {
|
|
/* Determine the number of points that should be sampled */
|
|
int sqrtVal = (int) (std::sqrt((float) pointCount) + 0.5f);
|
|
float invSqrtVal = 1.f / sqrtVal;
|
|
if (pointType == Grid || pointType == Stratified)
|
|
pointCount = sqrtVal*sqrtVal;
|
|
|
|
pcg32 rng;
|
|
positions.resize(3, pointCount);
|
|
weights.resize(1, pointCount);
|
|
|
|
for (int i=0; i<pointCount; ++i) {
|
|
int y = i / sqrtVal, x = i % sqrtVal;
|
|
Point2f sample;
|
|
|
|
switch (pointType) {
|
|
case Independent:
|
|
sample = Point2f(rng.nextFloat(), rng.nextFloat());
|
|
break;
|
|
|
|
case Grid:
|
|
sample = Point2f((x + 0.5f) * invSqrtVal, (y + 0.5f) * invSqrtVal);
|
|
break;
|
|
|
|
case Stratified:
|
|
sample = Point2f((x + rng.nextFloat()) * invSqrtVal,
|
|
(y + rng.nextFloat()) * invSqrtVal);
|
|
break;
|
|
}
|
|
|
|
auto result = warpPoint(sample);
|
|
positions.col(i) = result.first;
|
|
weights(0, i) = result.second;
|
|
}
|
|
}
|
|
|
|
static std::pair<BSDF *, BSDFQueryRecord>
|
|
create_microfacet_bsdf(float alpha, float kd, float bsdfAngle) {
|
|
PropertyList list;
|
|
list.setFloat("alpha", alpha);
|
|
list.setColor("kd", Color3f(kd));
|
|
auto * brdf = (BSDF *) NoriObjectFactory::createInstance("microfacet", list);
|
|
|
|
nori::Vector3f wi(std::sin(bsdfAngle), 0.f,
|
|
std::max(std::cos(bsdfAngle), 1e-4f));
|
|
wi = wi.normalized();
|
|
BSDFQueryRecord bRec(wi);
|
|
return { brdf, bRec };
|
|
}
|
|
};
|
|
|
|
|
|
struct Arcball {
|
|
using Quaternionf = Eigen::Quaternion<float, Eigen::DontAlign>;
|
|
|
|
Arcball(float speedFactor = 2.0f)
|
|
: m_active(false), m_lastPos(nori::Vector2i::Zero()), m_size(nori::Vector2i::Zero()),
|
|
m_quat(Quaternionf::Identity()),
|
|
m_incr(Quaternionf::Identity()),
|
|
m_speedFactor(speedFactor) { }
|
|
|
|
void setSize(nori::Vector2i size) { m_size = size; }
|
|
|
|
const nori::Vector2i &size() const { return m_size; }
|
|
|
|
void button(nori::Vector2i pos, bool pressed) {
|
|
m_active = pressed;
|
|
m_lastPos = pos;
|
|
if (!m_active)
|
|
m_quat = (m_incr * m_quat).normalized();
|
|
m_incr = Quaternionf::Identity();
|
|
}
|
|
|
|
bool motion(nori::Vector2i pos) {
|
|
if (!m_active)
|
|
return false;
|
|
|
|
/* Based on the rotation controller from AntTweakBar */
|
|
float invMinDim = 1.0f / m_size.minCoeff();
|
|
float w = (float) m_size.x(), h = (float) m_size.y();
|
|
|
|
float ox = (m_speedFactor * (2*m_lastPos.x() - w) + w) - w - 1.0f;
|
|
float tx = (m_speedFactor * (2*pos.x() - w) + w) - w - 1.0f;
|
|
float oy = (m_speedFactor * (h - 2*m_lastPos.y()) + h) - h - 1.0f;
|
|
float ty = (m_speedFactor * (h - 2*pos.y()) + h) - h - 1.0f;
|
|
|
|
ox *= invMinDim; oy *= invMinDim;
|
|
tx *= invMinDim; ty *= invMinDim;
|
|
|
|
nori::Vector3f v0(ox, oy, 1.0f), v1(tx, ty, 1.0f);
|
|
if (v0.squaredNorm() > 1e-4f && v1.squaredNorm() > 1e-4f) {
|
|
v0.normalize(); v1.normalize();
|
|
nori::Vector3f axis = v0.cross(v1);
|
|
float sa = std::sqrt(axis.dot(axis)),
|
|
ca = v0.dot(v1),
|
|
angle = std::atan2(sa, ca);
|
|
if (tx*tx + ty*ty > 1.0f)
|
|
angle *= 1.0f + 0.2f * (std::sqrt(tx*tx + ty*ty) - 1.0f);
|
|
m_incr = Eigen::AngleAxisf(angle, axis.normalized());
|
|
if (!std::isfinite(m_incr.norm()))
|
|
m_incr = Quaternionf::Identity();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Eigen::Matrix4f matrix() const {
|
|
Eigen::Matrix4f result2 = Eigen::Matrix4f::Identity();
|
|
result2.block<3,3>(0, 0) = (m_incr * m_quat).toRotationMatrix();
|
|
return result2;
|
|
}
|
|
|
|
|
|
private:
|
|
/// Whether or not this Arcball is currently active.
|
|
bool m_active;
|
|
|
|
/// The last click position (which triggered the Arcball to be active / non-active).
|
|
nori::Vector2i m_lastPos;
|
|
|
|
/// The size of this Arcball.
|
|
nori::Vector2i m_size;
|
|
|
|
/**
|
|
* The current stable state. When this Arcball is active, represents the
|
|
* state of this Arcball when \ref Arcball::button was called with
|
|
* ``down = true``.
|
|
*/
|
|
Quaternionf m_quat;
|
|
|
|
/// When active, tracks the overall update to the state. Identity when non-active.
|
|
Quaternionf m_incr;
|
|
|
|
/**
|
|
* The speed at which this Arcball rotates. Smaller values mean it rotates
|
|
* more slowly, higher values mean it rotates more quickly.
|
|
*/
|
|
float m_speedFactor;
|
|
|
|
public:
|
|
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
|
|
};
|
|
|
|
|
|
class WarpTestScreen : public Screen {
|
|
public:
|
|
|
|
WarpTestScreen(): Screen(Vector2i(800, 600), "warptest: Sampling and Warping"), m_bRec(nori::Vector3f()) {
|
|
inc_ref();
|
|
m_drawHistogram = false;
|
|
initializeGUI();
|
|
}
|
|
|
|
static float mapParameter(WarpType warpType, float parameterValue) {
|
|
if (warpType == Beckmann || warpType == MicrofacetBRDF)
|
|
parameterValue = std::exp(std::log(0.01f) * (1 - parameterValue) +
|
|
std::log(1.f) * parameterValue);
|
|
return parameterValue;
|
|
}
|
|
|
|
void refresh() {
|
|
PointType pointType = (PointType) m_pointTypeBox->selected_index();
|
|
WarpType warpType = (WarpType) m_warpTypeBox->selected_index();
|
|
float parameterValue = mapParameter(warpType, m_parameterSlider->value());
|
|
float parameter2Value = mapParameter(warpType, m_parameter2Slider->value());
|
|
m_pointCount = (int) std::pow(2.f, 15 * m_pointCountSlider->value() + 5);
|
|
|
|
if (warpType == MicrofacetBRDF) {
|
|
BSDF *ptr;
|
|
float bsdfAngle = M_PI * (m_angleSlider->value() - 0.5f);
|
|
std::tie(ptr, m_bRec) = WarpTest::create_microfacet_bsdf(
|
|
parameterValue, parameter2Value, bsdfAngle);
|
|
m_brdf.reset(ptr);
|
|
}
|
|
|
|
/* Generate the point positions */
|
|
nori::MatrixXf positions, values;
|
|
try {
|
|
WarpTest tester(warpType, parameterValue, m_brdf.get(), m_bRec);
|
|
tester.generatePoints(m_pointCount, pointType, positions, values);
|
|
} catch (const NoriException &e) {
|
|
m_warpTypeBox->set_selected_index(0);
|
|
refresh();
|
|
new MessageDialog(this, MessageDialog::Type::Warning, "Error", "An error occurred: " + std::string(e.what()));
|
|
return;
|
|
}
|
|
|
|
float value_scale = 0.f;
|
|
for (int i=0; i<m_pointCount; ++i)
|
|
value_scale = std::max(value_scale, values(0, i));
|
|
value_scale = 1.f / value_scale;
|
|
|
|
if (!m_brdfValueCheckBox->checked() || warpType != MicrofacetBRDF)
|
|
value_scale = 0.f;
|
|
|
|
if (warpType != Square) {
|
|
for (int i=0; i < m_pointCount; ++i) {
|
|
if (values(0, i) == 0.0f) {
|
|
positions.col(i) = nori::Vector3f::Constant(std::numeric_limits<float>::quiet_NaN());
|
|
continue;
|
|
}
|
|
positions.col(i) =
|
|
((value_scale == 0 ? 1.0f : (value_scale * values(0, i))) *
|
|
positions.col(i)) * 0.5f + nori::Vector3f(0.5f, 0.5f, 0.0f);
|
|
}
|
|
}
|
|
|
|
/* Generate a color gradient */
|
|
nori::MatrixXf colors(3, m_pointCount);
|
|
float colScale = 1.f / m_pointCount;
|
|
for (int i=0; i<m_pointCount; ++i)
|
|
colors.col(i) << i*colScale, 1-i*colScale, 0;
|
|
|
|
/* Upload points to GPU */
|
|
m_pointShader->set_buffer("position", VariableType::Float32, {(size_t) m_pointCount, 3}, positions.data());
|
|
m_pointShader->set_buffer("color", VariableType::Float32, {(size_t) m_pointCount, 3}, colors.data());
|
|
|
|
/* Upload lines to GPU */
|
|
if (m_gridCheckBox->checked()) {
|
|
int gridRes = (int) (std::sqrt((float) m_pointCount) + 0.5f);
|
|
int fineGridRes = 16*gridRes, idx = 0;
|
|
m_lineCount = 4 * (gridRes+1) * (fineGridRes+1);
|
|
positions.resize(3, m_lineCount);
|
|
float coarseScale = 1.f / gridRes, fineScale = 1.f / fineGridRes;
|
|
|
|
WarpTest tester(warpType, parameterValue, m_brdf.get(), m_bRec);
|
|
for (int i=0; i<=gridRes; ++i) {
|
|
for (int j=0; j<=fineGridRes; ++j) {
|
|
auto pt = tester.warpPoint(Point2f(j * fineScale, i * coarseScale));
|
|
positions.col(idx++) = value_scale == 0.f ? pt.first : (pt.first * pt.second * value_scale);
|
|
pt = tester.warpPoint(Point2f((j+1) * fineScale, i * coarseScale));
|
|
positions.col(idx++) = value_scale == 0.f ? pt.first : (pt.first * pt.second * value_scale);
|
|
pt = tester.warpPoint(Point2f(i*coarseScale, j * fineScale));
|
|
positions.col(idx++) = value_scale == 0.f ? pt.first : (pt.first * pt.second * value_scale);
|
|
pt = tester.warpPoint(Point2f(i*coarseScale, (j+1) * fineScale));
|
|
positions.col(idx++) = value_scale == 0.f ? pt.first : (pt.first * pt.second * value_scale);
|
|
}
|
|
}
|
|
if (warpType != Square) {
|
|
for (int i=0; i<m_lineCount; ++i)
|
|
positions.col(i) = positions.col(i) * 0.5f + nori::Vector3f(0.5f, 0.5f, 0.0f);
|
|
}
|
|
m_gridShader->set_buffer("position", VariableType::Float32, {(size_t) m_lineCount, 3}, positions.data());
|
|
}
|
|
|
|
int ctr = 0;
|
|
positions.resize(3, 106);
|
|
for (int i=0; i<=50; ++i) {
|
|
float angle1 = i * 2 * M_PI / 50;
|
|
float angle2 = (i+1) * 2 * M_PI / 50;
|
|
positions.col(ctr++) << std::cos(angle1)*.5f + 0.5f, std::sin(angle1)*.5f + 0.5f, 0.f;
|
|
positions.col(ctr++) << std::cos(angle2)*.5f + 0.5f, std::sin(angle2)*.5f + 0.5f, 0.f;
|
|
}
|
|
positions.col(ctr++) << 0.5f, 0.5f, 0.f;
|
|
positions.col(ctr++) << -m_bRec.wi.x() * 0.5f + 0.5f, -m_bRec.wi.y() * 0.5f + 0.5f, m_bRec.wi.z() * 0.5f;
|
|
positions.col(ctr++) << 0.5f, 0.5f, 0.f;
|
|
positions.col(ctr++) << m_bRec.wi.x() * 0.5f + 0.5f, m_bRec.wi.y() * 0.5f + 0.5f, m_bRec.wi.z() * 0.5f;
|
|
m_arrowShader->set_buffer("position", VariableType::Float32, {106, 3}, positions.data());
|
|
|
|
/* Update user interface */
|
|
std::string str;
|
|
if (m_pointCount > 1000000) {
|
|
m_pointCountBox->set_units("M");
|
|
str = tfm::format("%.2f", m_pointCount * 1e-6f);
|
|
} else if (m_pointCount > 1000) {
|
|
m_pointCountBox->set_units("K");
|
|
str = tfm::format("%.2f", m_pointCount * 1e-3f);
|
|
} else {
|
|
m_pointCountBox->set_units(" ");
|
|
str = tfm::format("%i", m_pointCount);
|
|
}
|
|
m_pointCountBox->set_value(str);
|
|
m_parameterBox->set_value(tfm::format("%.1g", parameterValue));
|
|
m_parameter2Box->set_value(tfm::format("%.1g", parameter2Value));
|
|
m_angleBox->set_value(tfm::format("%.1f", m_angleSlider->value() * 180-90));
|
|
m_parameterSlider->set_enabled(warpType == Beckmann || warpType == MicrofacetBRDF);
|
|
m_parameterBox->set_enabled(warpType == Beckmann || warpType == MicrofacetBRDF);
|
|
m_parameter2Slider->set_enabled(warpType == MicrofacetBRDF);
|
|
m_parameter2Box->set_enabled(warpType == MicrofacetBRDF);
|
|
m_angleBox->set_enabled(warpType == MicrofacetBRDF);
|
|
m_angleSlider->set_enabled(warpType == MicrofacetBRDF);
|
|
m_brdfValueCheckBox->set_enabled(warpType == MicrofacetBRDF);
|
|
m_pointCountSlider->set_value((std::log((float) m_pointCount) / std::log(2.f) - 5) / 15);
|
|
}
|
|
|
|
bool mouse_motion_event(const Vector2i &p, const Vector2i &rel,
|
|
int button, int modifiers) {
|
|
if (!Screen::mouse_motion_event(p, rel, button, modifiers))
|
|
m_arcball.motion(nori::Vector2i(p.x(), p.y()));
|
|
return true;
|
|
}
|
|
|
|
bool mouse_button_event(const Vector2i &p, int button, bool down, int modifiers) {
|
|
if (down && !m_window->visible()) {
|
|
m_drawHistogram = false;
|
|
m_window->set_visible(true);
|
|
return true;
|
|
}
|
|
if (!Screen::mouse_button_event(p, button, down, modifiers)) {
|
|
if (button == GLFW_MOUSE_BUTTON_1)
|
|
m_arcball.button(nori::Vector2i(p.x(), p.y()), down);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void draw_contents() {
|
|
/* Set up a perspective camera matrix */
|
|
Matrix4f view, proj, model(1.f);
|
|
view = Matrix4f::look_at(Vector3f(0, 0, 4), Vector3f(0, 0, 0), Vector3f(0, 1, 0));
|
|
const float viewAngle = 30, near_clip = 0.01, far_clip = 100;
|
|
proj = Matrix4f::perspective(viewAngle / 360.0f * M_PI, near_clip, far_clip,
|
|
(float) m_size.x() / (float) m_size.y());
|
|
model = Matrix4f::translate(Vector3f(-0.5f, -0.5f, 0.0f)) * model;
|
|
|
|
Matrix4f arcball_ng(1.f);
|
|
memcpy(arcball_ng.m, m_arcball.matrix().data(), sizeof(float) * 16);
|
|
model = arcball_ng * model;
|
|
m_renderPass->resize(framebuffer_size());
|
|
m_renderPass->begin();
|
|
|
|
if (m_drawHistogram) {
|
|
/* Render the histograms */
|
|
WarpType warpType = (WarpType) m_warpTypeBox->selected_index();
|
|
const int spacer = 20;
|
|
const int histWidth = (width() - 3*spacer) / 2;
|
|
const int histHeight = (warpType == Square || warpType == Disk || warpType == Tent) ? histWidth : histWidth / 2;
|
|
const int verticalOffset = (height() - histHeight) / 2;
|
|
|
|
drawHistogram(Vector2i(spacer, verticalOffset), Vector2i(histWidth, histHeight), m_textures[0].get());
|
|
drawHistogram(Vector2i(2*spacer + histWidth, verticalOffset), Vector2i(histWidth, histHeight), m_textures[1].get());
|
|
|
|
auto ctx = m_nvg_context;
|
|
nvgBeginFrame(ctx, m_size[0], m_size[1], m_pixel_ratio);
|
|
nvgBeginPath(ctx);
|
|
nvgRect(ctx, spacer, verticalOffset + histHeight + spacer, width()-2*spacer, 70);
|
|
nvgFillColor(ctx, m_testResult.first ? Color(100, 255, 100, 100) : Color(255, 100, 100, 100));
|
|
nvgFill(ctx);
|
|
nvgFontSize(ctx, 24.0f);
|
|
nvgFontFace(ctx, "sans-bold");
|
|
nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_TOP);
|
|
nvgFillColor(ctx, Color(255, 255));
|
|
nvgText(ctx, spacer + histWidth / 2, verticalOffset - 3 * spacer,
|
|
"Sample histogram", nullptr);
|
|
nvgText(ctx, 2 * spacer + (histWidth * 3) / 2, verticalOffset - 3 * spacer,
|
|
"Integrated density", nullptr);
|
|
nvgStrokeColor(ctx, Color(255, 255));
|
|
nvgStrokeWidth(ctx, 2);
|
|
nvgBeginPath(ctx);
|
|
nvgRect(ctx, spacer, verticalOffset, histWidth, histHeight);
|
|
nvgRect(ctx, 2 * spacer + histWidth, verticalOffset, histWidth,
|
|
histHeight);
|
|
nvgStroke(ctx);
|
|
nvgFontSize(ctx, 20.0f);
|
|
nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_TOP);
|
|
|
|
float bounds[4];
|
|
nvgTextBoxBounds(ctx, 0, 0, width() - 2 * spacer,
|
|
m_testResult.second.c_str(), nullptr, bounds);
|
|
nvgTextBox(
|
|
ctx, spacer, verticalOffset + histHeight + spacer + (70 - bounds[3])/2,
|
|
width() - 2 * spacer, m_testResult.second.c_str(), nullptr);
|
|
nvgEndFrame(ctx);
|
|
} else {
|
|
/* Render the point set */
|
|
Matrix4f mvp = proj * view * model;
|
|
m_pointShader->set_uniform("mvp", mvp);
|
|
glPointSize(2);
|
|
m_renderPass->set_depth_test(RenderPass::DepthTest::Less, true);
|
|
m_pointShader->begin();
|
|
m_pointShader->draw_array(nanogui::Shader::PrimitiveType::Point, 0, m_pointCount);
|
|
m_pointShader->end();
|
|
|
|
bool drawGrid = m_gridCheckBox->checked();
|
|
if (drawGrid) {
|
|
m_gridShader->set_uniform("mvp", mvp);
|
|
m_gridShader->begin();
|
|
m_gridShader->draw_array(nanogui::Shader::PrimitiveType::Line, 0, m_lineCount);
|
|
m_gridShader->end();
|
|
}
|
|
if (m_warpTypeBox->selected_index() == MicrofacetBRDF) {
|
|
m_arrowShader->set_uniform("mvp", mvp);
|
|
m_arrowShader->begin();
|
|
m_arrowShader->draw_array(nanogui::Shader::PrimitiveType::Line, 0, 106);
|
|
m_arrowShader->end();
|
|
}
|
|
}
|
|
m_renderPass->end();
|
|
}
|
|
|
|
void drawHistogram(const nanogui::Vector2i &pos_, const nanogui::Vector2i &size_, Texture *texture) {
|
|
Vector2f s = -(Vector2f(pos_) + Vector2f(0.25f, 0.25f)) / Vector2f(size_);
|
|
Vector2f e = Vector2f(size()) / Vector2f(size_) + s;
|
|
Matrix4f mvp = Matrix4f::ortho(s.x(), e.x(), s.y(), e.y(), -1, 1);
|
|
m_renderPass->set_depth_test(RenderPass::DepthTest::Always, true);
|
|
m_histogramShader->set_uniform("mvp", mvp);
|
|
m_histogramShader->set_texture("tex", texture);
|
|
m_histogramShader->begin();
|
|
m_histogramShader->draw_array(nanogui::Shader::PrimitiveType::Triangle, 0, 6, true);
|
|
m_histogramShader->end();
|
|
}
|
|
|
|
void runTest() {
|
|
// Prepare and run test, passing parameters from UI.
|
|
WarpType warpType = (WarpType) m_warpTypeBox->selected_index();
|
|
float parameterValue = mapParameter(warpType, m_parameterSlider->value());
|
|
|
|
WarpTest tester(warpType, parameterValue, m_brdf.get(), m_bRec);
|
|
m_testResult = tester.run();
|
|
|
|
float maxValue = 0, minValue = std::numeric_limits<float>::infinity();
|
|
for (int i=0; i<tester.res; ++i) {
|
|
maxValue = std::max(maxValue,
|
|
(float) std::max(tester.obsFrequencies[i], tester.expFrequencies[i]));
|
|
minValue = std::min(minValue,
|
|
(float) std::min(tester.obsFrequencies[i], tester.expFrequencies[i]));
|
|
}
|
|
minValue /= 2;
|
|
float texScale = 1/(maxValue - minValue);
|
|
|
|
/* Upload histograms to GPU */
|
|
std::unique_ptr<float[]> buffer(new float[tester.res]);
|
|
for (int k=0; k<2; ++k) {
|
|
for (int i=0; i<tester.res; ++i)
|
|
buffer[i] = ((k == 0 ? tester.obsFrequencies[i]
|
|
: tester.expFrequencies[i]) - minValue) * texScale;
|
|
m_textures[k] = new Texture(Texture::PixelFormat::R,
|
|
Texture::ComponentFormat::Float32,
|
|
nanogui::Vector2i(tester.xres, tester.yres),
|
|
Texture::InterpolationMode::Nearest,
|
|
Texture::InterpolationMode::Nearest);
|
|
m_textures[k]->upload((uint8_t *) buffer.get());
|
|
}
|
|
m_drawHistogram = true;
|
|
m_window->set_visible(false);
|
|
}
|
|
|
|
void initializeGUI() {
|
|
m_window = new Window(this, "Warp tester");
|
|
m_window->set_position(Vector2i(15, 15));
|
|
m_window->set_layout(new GroupLayout());
|
|
|
|
set_resize_callback([&](Vector2i size) {
|
|
m_arcball.setSize(nori::Vector2i(size.x(), size.y()));
|
|
});
|
|
|
|
m_arcball.setSize(nori::Vector2i(m_size.x(), m_size.y()));
|
|
|
|
new Label(m_window, "Input point set", "sans-bold");
|
|
|
|
/* Create an empty panel with a horizontal layout */
|
|
Widget *panel = new Widget(m_window);
|
|
panel->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 20));
|
|
|
|
/* Add a slider and set defaults */
|
|
m_pointCountSlider = new Slider(panel);
|
|
m_pointCountSlider->set_fixed_width(55);
|
|
m_pointCountSlider->set_callback([&](float) { refresh(); });
|
|
|
|
/* Add a textbox and set defaults */
|
|
m_pointCountBox = new TextBox(panel);
|
|
m_pointCountBox->set_fixed_size(Vector2i(80, 25));
|
|
|
|
m_pointTypeBox = new ComboBox(m_window, { "Independent", "Grid", "Stratified" });
|
|
m_pointTypeBox->set_callback([&](int) { refresh(); });
|
|
|
|
new Label(m_window, "Warping method", "sans-bold");
|
|
m_warpTypeBox = new ComboBox(m_window, { "Square", "Tent", "Disk", "Sphere", "Hemisphere (unif.)",
|
|
"Hemisphere (cos)", "Beckmann distr.", "Microfacet BRDF" });
|
|
m_warpTypeBox->set_callback([&](int) { refresh(); });
|
|
|
|
panel = new Widget(m_window);
|
|
panel->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 20));
|
|
m_parameterSlider = new Slider(panel);
|
|
m_parameterSlider->set_fixed_width(55);
|
|
m_parameterSlider->set_callback([&](float) { refresh(); });
|
|
m_parameterBox = new TextBox(panel);
|
|
m_parameterBox->set_fixed_size(Vector2i(80, 25));
|
|
panel = new Widget(m_window);
|
|
panel->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 20));
|
|
m_parameter2Slider = new Slider(panel);
|
|
m_parameter2Slider->set_fixed_width(55);
|
|
m_parameter2Slider->set_callback([&](float) { refresh(); });
|
|
m_parameter2Box = new TextBox(panel);
|
|
m_parameter2Box->set_fixed_size(Vector2i(80, 25));
|
|
m_gridCheckBox = new CheckBox(m_window, "Visualize warped grid");
|
|
m_gridCheckBox->set_callback([&](bool) { refresh(); });
|
|
|
|
new Label(m_window, "BSDF parameters", "sans-bold");
|
|
|
|
panel = new Widget(m_window);
|
|
panel->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 0, 20));
|
|
|
|
m_angleSlider = new Slider(panel);
|
|
m_angleSlider->set_fixed_width(55);
|
|
m_angleSlider->set_callback([&](float) { refresh(); });
|
|
m_angleBox = new TextBox(panel);
|
|
m_angleBox->set_fixed_size(Vector2i(80, 25));
|
|
m_angleBox->set_units(utf8(0x00B0).data());
|
|
|
|
m_brdfValueCheckBox = new CheckBox(m_window, "Visualize BRDF values");
|
|
m_brdfValueCheckBox->set_callback([&](bool) { refresh(); });
|
|
|
|
new Label(m_window,
|
|
std::string(utf8(0x03C7).data()) +
|
|
std::string(utf8(0x00B2).data()) + " hypothesis test",
|
|
"sans-bold");
|
|
|
|
Button *testBtn = new Button(m_window, "Run", FA_CHECK);
|
|
testBtn->set_background_color(Color(0, 255, 0, 25));
|
|
testBtn->set_callback([&]{
|
|
try {
|
|
runTest();
|
|
} catch (const NoriException &e) {
|
|
new MessageDialog(this, MessageDialog::Type::Warning, "Error", "An error occurred: " + std::string(e.what()));
|
|
}
|
|
});
|
|
|
|
m_renderPass = new RenderPass({ this });
|
|
m_renderPass->set_clear_color(0, Color(0.f, 0.f, 0.f, 1.f));
|
|
|
|
perform_layout();
|
|
|
|
m_pointShader = new Shader(
|
|
m_renderPass,
|
|
"Point shader",
|
|
|
|
/* Vertex shader */
|
|
R"(#version 330
|
|
uniform mat4 mvp;
|
|
in vec3 position;
|
|
in vec3 color;
|
|
out vec3 frag_color;
|
|
void main() {
|
|
gl_Position = mvp * vec4(position, 1.0);
|
|
if (isnan(position.r)) /* nan (missing value) */
|
|
frag_color = vec3(0.0);
|
|
else
|
|
frag_color = color;
|
|
})",
|
|
|
|
/* Fragment shader */
|
|
R"(#version 330
|
|
in vec3 frag_color;
|
|
out vec4 out_color;
|
|
void main() {
|
|
if (frag_color == vec3(0.0))
|
|
discard;
|
|
out_color = vec4(frag_color, 1.0);
|
|
})"
|
|
);
|
|
|
|
m_gridShader = new Shader(
|
|
m_renderPass,
|
|
"Grid shader",
|
|
|
|
/* Vertex shader */
|
|
R"(#version 330
|
|
uniform mat4 mvp;
|
|
in vec3 position;
|
|
void main() {
|
|
gl_Position = mvp * vec4(position, 1.0);
|
|
})",
|
|
|
|
/* Fragment shader */
|
|
R"(#version 330
|
|
out vec4 out_color;
|
|
void main() {
|
|
out_color = vec4(vec3(1.0), 0.4);
|
|
})", Shader::BlendMode::AlphaBlend
|
|
);
|
|
|
|
m_arrowShader = new Shader(
|
|
m_renderPass,
|
|
"Arrow shader",
|
|
|
|
/* Vertex shader */
|
|
R"(#version 330
|
|
uniform mat4 mvp;
|
|
in vec3 position;
|
|
void main() {
|
|
gl_Position = mvp * vec4(position, 1.0);
|
|
})",
|
|
|
|
/* Fragment shader */
|
|
R"(#version 330
|
|
out vec4 out_color;
|
|
void main() {
|
|
out_color = vec4(vec3(1.0), 0.4);
|
|
})"
|
|
);
|
|
|
|
m_histogramShader = new Shader(
|
|
m_renderPass,
|
|
"Histogram shader",
|
|
|
|
/* Vertex shader */
|
|
R"(#version 330
|
|
uniform mat4 mvp;
|
|
in vec2 position;
|
|
out vec2 uv;
|
|
void main() {
|
|
gl_Position = mvp * vec4(position, 0.0, 1.0);
|
|
uv = position;
|
|
})",
|
|
|
|
/* Fragment shader */
|
|
R"(#version 330
|
|
out vec4 out_color;
|
|
uniform sampler2D tex;
|
|
in vec2 uv;
|
|
/* http://paulbourke.net/texture_colour/colourspace/ */
|
|
vec3 colormap(float v, float vmin, float vmax) {
|
|
vec3 c = vec3(1.0);
|
|
if (v < vmin)
|
|
v = vmin;
|
|
if (v > vmax)
|
|
v = vmax;
|
|
float dv = vmax - vmin;
|
|
|
|
if (v < (vmin + 0.25 * dv)) {
|
|
c.r = 0.0;
|
|
c.g = 4.0 * (v - vmin) / dv;
|
|
} else if (v < (vmin + 0.5 * dv)) {
|
|
c.r = 0.0;
|
|
c.b = 1.0 + 4.0 * (vmin + 0.25 * dv - v) / dv;
|
|
} else if (v < (vmin + 0.75 * dv)) {
|
|
c.r = 4.0 * (v - vmin - 0.5 * dv) / dv;
|
|
c.b = 0.0;
|
|
} else {
|
|
c.g = 1.0 + 4.0 * (vmin + 0.75 * dv - v) / dv;
|
|
c.b = 0.0;
|
|
}
|
|
return c;
|
|
}
|
|
void main() {
|
|
float value = texture(tex, uv).r;
|
|
out_color = vec4(colormap(value, 0.0, 1.0), 1.0);
|
|
})"
|
|
);
|
|
|
|
/* Upload a single quad */
|
|
uint32_t indices[3 * 2] = {
|
|
0, 1, 2,
|
|
2, 3, 0
|
|
};
|
|
float positions[2 * 4] = {
|
|
0.f, 0.f,
|
|
1.f, 0.f,
|
|
1.f, 1.f,
|
|
0.f, 1.f
|
|
};
|
|
m_histogramShader->set_buffer("indices", VariableType::UInt32, {3 * 2}, indices);
|
|
m_histogramShader->set_buffer("position", VariableType::Float32, {4, 2}, positions);
|
|
|
|
/* Set default and register slider callback */
|
|
m_pointCountSlider->set_value(7.f / 15.f);
|
|
m_parameterSlider->set_value(.5f);
|
|
m_parameter2Slider->set_value(0.f);
|
|
m_angleSlider->set_value(.5f);
|
|
|
|
refresh();
|
|
set_visible(true);
|
|
draw_all();
|
|
}
|
|
|
|
private:
|
|
nanogui::ref<Shader> m_pointShader, m_gridShader, m_histogramShader, m_arrowShader;
|
|
Window *m_window;
|
|
Slider *m_pointCountSlider, *m_parameterSlider, *m_parameter2Slider, *m_angleSlider;
|
|
TextBox *m_pointCountBox, *m_parameterBox, *m_parameter2Box, *m_angleBox;
|
|
nanogui::ref<nanogui::Texture> m_textures[2];
|
|
ComboBox *m_pointTypeBox;
|
|
ComboBox *m_warpTypeBox;
|
|
CheckBox *m_gridCheckBox;
|
|
CheckBox *m_brdfValueCheckBox;
|
|
Arcball m_arcball;
|
|
int m_pointCount, m_lineCount;
|
|
bool m_drawHistogram;
|
|
std::unique_ptr<BSDF> m_brdf;
|
|
BSDFQueryRecord m_bRec;
|
|
std::pair<bool, std::string> m_testResult;
|
|
nanogui::ref<RenderPass> m_renderPass;
|
|
};
|
|
|
|
|
|
std::tuple<WarpType, float, float> parse_arguments(int argc, char **argv) {
|
|
WarpType tp = WarpTypeCount;
|
|
for (int i = 0; i < WarpTypeCount; ++i) {
|
|
if (strcmp(kWarpTypeNames[i].c_str(), argv[1]) == 0)
|
|
tp = WarpType(i);
|
|
}
|
|
if (tp >= WarpTypeCount)
|
|
throw std::runtime_error("Invalid warp type!");
|
|
|
|
float value = 0.f, value2 = 0.f;
|
|
if (argc > 2)
|
|
value = std::stof(argv[2]);
|
|
if (argc > 3)
|
|
value2 = std::stof(argv[3]);
|
|
|
|
return { tp, value, value2 };
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc <= 1) {
|
|
// GUI mode
|
|
nanogui::init();
|
|
WarpTestScreen *screen = new WarpTestScreen();
|
|
nanogui::mainloop();
|
|
delete screen;
|
|
nanogui::shutdown();
|
|
return 0;
|
|
}
|
|
|
|
// CLI mode
|
|
WarpType warpType;
|
|
float paramValue, param2Value;
|
|
std::unique_ptr<BSDF> bsdf;
|
|
auto bRec = BSDFQueryRecord(nori::Vector3f());
|
|
std::tie(warpType, paramValue, param2Value) = parse_arguments(argc, argv);
|
|
if (warpType == MicrofacetBRDF) {
|
|
float bsdfAngle = M_PI * 0.f;
|
|
BSDF *ptr;
|
|
std::tie(ptr, bRec) = WarpTest::create_microfacet_bsdf(
|
|
paramValue, param2Value, bsdfAngle);
|
|
bsdf.reset(ptr);
|
|
}
|
|
|
|
std::string extra = "";
|
|
if (param2Value > 0)
|
|
extra = tfm::format(", second parameter value = %f", param2Value);
|
|
std::cout << tfm::format(
|
|
"Testing warp %s, parameter value = %f%s",
|
|
kWarpTypeNames[int(warpType)], paramValue, extra
|
|
) << std::endl;
|
|
WarpTest tester(warpType, paramValue, bsdf.get(), bRec);
|
|
auto res = tester.run();
|
|
if (res.first)
|
|
return 0;
|
|
|
|
std::cout << tfm::format("warptest failed: %s", res.second) << std::endl;
|
|
return 1;
|
|
}
|