Nori  24
gui.cpp
1 /*
2  This file is part of Nori, a simple educational ray tracer
3 
4  Copyright (c) 2015 by Wenzel Jakob, Romain Prévost
5 
6  Nori is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License Version 3
8  as published by the Free Software Foundation.
9 
10  Nori is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  GNU General Public License for more details.
14 
15  You should have received a copy of the GNU General Public License
16  along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include <nori/gui.h>
20 #include <nori/block.h>
21 #include <nori/parser.h>
22 #include <nori/bitmap.h>
23 #include <nanogui/shader.h>
24 #include <nanogui/label.h>
25 #include <nanogui/slider.h>
26 #include <nanogui/button.h>
27 #include <nanogui/layout.h>
28 #include <nanogui/icons.h>
29 #include <nanogui/renderpass.h>
30 #include <nanogui/progressbar.h>
31 #include <nanogui/texture.h>
32 
33 #include <nanogui/opengl.h>
34 
35 #include <filesystem/resolver.h>
36 #include <fstream>
37 
38 NORI_NAMESPACE_BEGIN
39 
40 #define PANEL_HEIGHT 50
41 
42 NoriCanvas::NoriCanvas(nanogui::Widget* parent, const ImageBlock& block) : nanogui::Canvas(parent, 1), m_block(block) {
43  using namespace nanogui;
44 
45  m_shader = new Shader(
46  render_pass(),
47  /* An identifying name */
48  "Tonemapper",
49  /* Vertex shader */
50  R"(#version 330
51  uniform ivec2 size;
52  uniform int borderSize;
53 
54  in vec2 position;
55  out vec2 uv;
56  void main() {
57  gl_Position = vec4(position.x * 2 - 1, position.y * 2 - 1, 0.0, 1.0);
58 
59  // Crop away image border (due to pixel filter)
60  vec2 total_size = size + 2 * borderSize;
61  vec2 scale = size / total_size;
62  uv = vec2(position.x * scale.x + borderSize / total_size.x,
63  1 - (position.y * scale.y + borderSize / total_size.y));
64  })",
65  /* Fragment shader */
66  R"(#version 330
67  uniform sampler2D source;
68  uniform float scale;
69  in vec2 uv;
70  out vec4 out_color;
71  float toSRGB(float value) {
72  if (value < 0.0031308)
73  return 12.92 * value;
74  return 1.055 * pow(value, 0.41666) - 0.055;
75  }
76  void main() {
77  vec4 color = texture(source, uv);
78  color *= scale / color.w;
79  out_color = vec4(toSRGB(color.r), toSRGB(color.g), toSRGB(color.b), 1);
80  })"
81  );
82 
83  // Draw 2 triangles
84  uint32_t indices[3 * 2] = {
85  0, 1, 2,
86  2, 3, 0
87  };
88  float positions[2 * 4] = {
89  0.f, 0.f,
90  1.f, 0.f,
91  1.f, 1.f,
92  0.f, 1.f
93  };
94 
95  m_shader->set_buffer("indices", VariableType::UInt32, { 3 * 2 }, indices);
96  m_shader->set_buffer("position", VariableType::Float32, { 4, 2 }, positions);
97 
98  const Vector2i& size = m_block.getSize();
99  m_shader->set_uniform("size", nanogui::Vector2i(size.x(), size.y()));
100  m_shader->set_uniform("borderSize", m_block.getBorderSize());
101 
102  update();
103 }
104 
105 
106 void NoriCanvas::draw_contents() {
107  // Reload the partially rendered image onto the GPU
108  m_block.lock();
109  m_shader->set_uniform("scale", m_scale);
110  m_texture->upload((uint8_t*)m_block.data());
111  m_shader->set_texture("source", m_texture);
112  m_shader->begin();
113  m_shader->draw_array(nanogui::Shader::PrimitiveType::Triangle, 0, 6, true);
114  m_shader->end();
115  m_block.unlock();
116 }
117 
118 
119 void NoriCanvas::update() {
120  using namespace nanogui;
121  // Assumes that m_block stays static
122  const Vector2i& size = m_block.getSize();
123  const int borderSize = m_block.getBorderSize();
124 
125  // Allocate texture memory for the rendered image
126  m_texture = new Texture(
127  Texture::PixelFormat::RGBA,
128  Texture::ComponentFormat::Float32,
129  nanogui::Vector2i(size.x() + 2 * borderSize,
130  size.y() + 2 * borderSize),
131  Texture::InterpolationMode::Bilinear,
132  Texture::InterpolationMode::Bilinear);
133 }
134 
135 NoriScreen::NoriScreen(ImageBlock& block)
136  : nanogui::Screen(nanogui::Vector2i(block.getSize().x(), block.getSize().y() + PANEL_HEIGHT), "Nori", true),
137  m_block(block), m_renderThread(m_block)
138 {
139  using namespace nanogui;
140 
141  set_size(nanogui::Vector2i(block.getSize().x(), block.getSize().y() + PANEL_HEIGHT));
142 
143  /* Add some UI elements to adjust the exposure value */
144  panel = new Widget(this);
145  panel->set_width(m_block.getSize().x());
146  panel->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 10, 10));
147  Button* buttonOpen = new Button(panel, "", FA_FOLDER);
148 
149  buttonOpen->set_callback(
150  [this]() {
151  using FileType = std::pair<std::string, std::string>;
152  std::vector<FileType> filetypes = { FileType("xml", "Nori Scene File"), FileType("exr", "EXR Image File") };
153  std::string filename = nanogui::file_dialog(filetypes, false);
154  drop_event({ filename });
155  }
156  );
157  buttonOpen->set_tooltip("Open XML or EXR file (Ctrl+O)");
158 
159  m_progressBar = new ProgressBar(panel);
160  m_progressBar->set_fixed_size(nanogui::Vector2i(200, 10));
161  Button* buttonStop = new Button(panel, "", FA_STOP);
162  buttonStop->set_callback(
163  [this]() {
164  m_renderThread.stopRendering();
165  }
166  );
167  buttonStop->set_tooltip("Abort rendering (Ctrl+Z)");
168 
169  new Label(panel, "Exposure: ", "sans-bold");
170  m_slider = new Slider(panel);
171  m_slider->set_value(0.5f);
172  m_slider->set_fixed_width(120);
173  m_slider->set_callback(
174  [&](float value) {
175  m_scale = std::pow(2.f, (value - 0.5f) * 20);
176  m_render_canvas->set_scale(m_scale);
177  }
178  );
179 
180  Button* buttonSave = new Button(panel, "", FA_SAVE);
181  buttonSave->set_callback(
182  [this]() {
183  using FileType = std::pair<std::string, std::string>;
184  std::vector<FileType> filetypes = { FileType("", "Image File Name") };
185  std::string filename;
186  // Keep asking for a filename until the user cancels or the file does not exist
187  auto file_exists = [](const std::string& name) {
188  std::ifstream f(name.c_str());
189  return f.good();
190  };
191  while (true) {
192  filename = nanogui::file_dialog(filetypes, true);
193  if (filename.empty() || (!file_exists(filename + ".png") && !file_exists(filename + ".exr")))
194  break;
195  cerr << "Error: file \"" << filename << "\" already exists!" << endl;
196  }
197  if (!filename.empty()) {
198  m_block.lock();
199  std::unique_ptr<Bitmap> bitmap(m_block.toBitmap());
200  m_block.unlock();
201  bitmap->array() *= m_scale; // apply exposure
202  bitmap->save(filename);
203  }
204  }
205  );
206  buttonSave->set_tooltip("Save rendering with the selected exposure to disk");
207 
208  set_resize_callback([this](nanogui::Vector2i) { requestLayoutUpdate(); });
209 
210  m_render_canvas = new NoriCanvas(this, m_block);
211  m_render_canvas->set_background_color({ 100, 100, 100, 255 });
212 
213  updateLayout();
214 
215  set_visible(true);
216  draw_all();
217 }
218 
219 
220 void NoriScreen::draw_contents() {
221  if (m_requiresLayoutUpdate) {
222  updateLayout();
223  m_requiresLayoutUpdate = false;
224  }
225 
226  if (m_progressBar) {
227  m_progressBar->set_value(m_renderThread.getProgress());
228  }
229  nanogui::Screen::draw_contents();
230 }
231 
232 
233 void NoriScreen::updateLayout() {
234  perform_layout();
235 
236  {
237  panel->set_position(nanogui::Vector2i((m_size.x() - panel->size().x()) / 2, 0));
238  }
239 
240  nanogui::Vector2i contentOffset(0, PANEL_HEIGHT);
241 
242  {
243  nanogui::Vector2i contentWindow = m_size - contentOffset;
244  nanogui::Vector2f blockSize(m_block.getSize().x(), m_block.getSize().y());
245  nanogui::Vector2f ratio = nanogui::Vector2f(contentWindow) / blockSize;
246  float minRatio = std::min(ratio.x(), ratio.y());
247 
248  // 1% margins
249  nanogui::Vector2i canvasSize = blockSize * minRatio * 0.98f;
250  nanogui::Vector2i canvasPosition = (contentWindow - canvasSize) / 2 + contentOffset;
251 
252  m_render_canvas->set_fixed_size(canvasSize);
253  m_render_canvas->set_position(canvasPosition);
254  }
255 
256  perform_layout();
257 }
258 
259 
260 bool NoriScreen::drop_event(const std::vector<std::string>& filenames) {
261  if (filenames.size() > 0) {
262  std::string filename = filenames[0];
263  if (filename.size() == 0) {
264  return true;
265  }
266  filesystem::path path(filename);
267 
268  if (path.extension() == "xml") {
269  /* Render the XML scene file */
270  openXML(filename);
271  } else if (path.extension() == "exr") {
272  /* Alternatively, provide a basic OpenEXR image viewer */
273  openEXR(filename);
274  } else {
275  cerr << "Error: unknown file \"" << filename
276  << "\", expected an extension of type .xml or .exr" << endl;
277  }
278 
279  }
280  return true;
281 }
282 
283 bool NoriScreen::keyboard_event(int key, int scancode, int action, int modifiers) {
284  const bool press = (action == GLFW_PRESS);
285  if (press && key == GLFW_KEY_O && modifiers & GLFW_MOD_CONTROL) {
286  using FileType = std::pair<std::string, std::string>;
287  std::vector<FileType> filetypes = { FileType("xml", "Nori Scene File"), FileType("exr", "EXR Image File") };
288  std::string filename = nanogui::file_dialog(filetypes, false);
289  drop_event({ filename });
290  return true;
291  }
292  if (press && key == GLFW_KEY_Z && modifiers & GLFW_MOD_CONTROL) {
293  m_renderThread.stopRendering();
294  return true;
295  }
296 
297  return nanogui::Screen::keyboard_event(key, scancode, action, modifiers);
298 }
299 
300 void NoriScreen::adjustWindow(Vector2i blockSize) {
301 
302  const nanogui::Vector2i contentOffset(0, PANEL_HEIGHT);
303  const nanogui::Vector2f contentWindow = m_size - contentOffset;
304 
305  // Heuristic - keep the total area them same
306  float nPixels = contentWindow.x() * contentWindow.y();
307  float ratio = std::sqrt(nPixels / ((float)blockSize.x() * blockSize.y()));
308 
309  // Do not make smaller than the panel
310  ratio = std::max(ratio, panel->size().x() / (float)blockSize.x());
311 
312  nanogui::Vector2i newSize(blockSize.x() * ratio + 1, blockSize.y() * ratio + PANEL_HEIGHT);
313  set_size(newSize);
314 
315 }
316 
317 void NoriScreen::openXML(const std::string& filename) {
318 
319  if(m_renderThread.isBusy()) {
320  cerr << "Error: rendering in progress, you need to wait until it's done" << endl;
321  return;
322  }
323 
324  try {
325 
326  m_renderThread.renderScene(filename);
327 
328  m_block.lock();
329  Vector2i bsize = m_block.getSize();
330  m_render_canvas->update();
331  m_block.unlock();
332 
333  adjustWindow(bsize);
334  requestLayoutUpdate();
335 
336  } catch (const std::exception &e) {
337  cerr << "Fatal error: " << e.what() << endl;
338  }
339 
340 }
341 
342 void NoriScreen::openEXR(const std::string& filename) {
343 
344  if(m_renderThread.isBusy()) {
345  cerr << "Error: rendering in progress, you need to wait until it's done" << endl;
346  return;
347  }
348 
349  Bitmap bitmap(filename);
350  m_block.lock();
351  m_block.init(Vector2i(bitmap.cols(), bitmap.rows()), nullptr);
352  m_block.fromBitmap(bitmap);
353  Vector2i bsize = m_block.getSize();
354  m_render_canvas->update();
355  m_block.unlock();
356 
357  adjustWindow(bsize);
358  requestLayoutUpdate();
359 }
360 
361 NORI_NAMESPACE_END
Stores a RGB high dynamic-range bitmap.
Definition: bitmap.h:32
Weighted pixel storage for a rectangular subregion of an image.
Definition: block.h:48
void unlock() const
Unlock the image block.
Definition: block.h:112
const Vector2i & getSize() const
Return the size of the block within the main image.
Definition: block.h:75
void fromBitmap(const Bitmap &bitmap)
Convert a bitmap into an image block.
Definition: block.cpp:84
void lock() const
Lock the image block (using an internal mutex)
Definition: block.h:109
Definition: gui.h:31
void renderScene(const std::string &filename)
Definition: render.cpp:102
Superclass of all texture.
Definition: texture.h:30