Nori  23
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  const Vector2i& size = m_block.getSize();
110  m_shader->set_uniform("scale", m_scale);
111  m_texture->upload((uint8_t*)m_block.data());
112  m_shader->set_texture("source", m_texture);
113  m_shader->begin();
114  m_shader->draw_array(nanogui::Shader::PrimitiveType::Triangle, 0, 6, true);
115  m_shader->end();
116  m_block.unlock();
117 }
118 
119 
120 void NoriCanvas::update() {
121  using namespace nanogui;
122  // Assumes that m_block stays static
123  const Vector2i& size = m_block.getSize();
124  const int borderSize = m_block.getBorderSize();
125 
126  // Allocate texture memory for the rendered image
127  m_texture = new Texture(
128  Texture::PixelFormat::RGBA,
129  Texture::ComponentFormat::Float32,
130  nanogui::Vector2i(size.x() + 2 * borderSize,
131  size.y() + 2 * borderSize),
132  Texture::InterpolationMode::Nearest,
133  Texture::InterpolationMode::Nearest);
134 }
135 
136 NoriScreen::NoriScreen(ImageBlock& block)
137  : nanogui::Screen(nanogui::Vector2i(block.getSize().x(), block.getSize().y() + PANEL_HEIGHT), "Nori", true),
138  m_block(block), m_renderThread(m_block)
139 {
140  using namespace nanogui;
141 
142  set_size(nanogui::Vector2i(block.getSize().x(), block.getSize().y() + PANEL_HEIGHT));
143 
144  /* Add some UI elements to adjust the exposure value */
145  panel = new Widget(this);
146  panel->set_width(m_block.getSize().x());
147  panel->set_layout(new BoxLayout(Orientation::Horizontal, Alignment::Middle, 10, 10));
148  Button* buttonOpen = new Button(panel, "", FA_FOLDER);
149 
150  buttonOpen->set_callback(
151  [this]() {
152  using FileType = std::pair<std::string, std::string>;
153  std::vector<FileType> filetypes = { FileType("xml", "Nori Scene File"), FileType("exr", "EXR Image File") };
154  std::string filename = nanogui::file_dialog(filetypes, false);
155  drop_event({ filename });
156  }
157  );
158  buttonOpen->set_tooltip("Open XML or EXR file (Ctrl+O)");
159 
160  m_progressBar = new ProgressBar(panel);
161  m_progressBar->set_fixed_size(nanogui::Vector2i(200, 10));
162  Button* buttonStop = new Button(panel, "", FA_STOP);
163  buttonStop->set_callback(
164  [this]() {
165  m_renderThread.stopRendering();
166  }
167  );
168  buttonStop->set_tooltip("Abort rendering (Ctrl+Z)");
169 
170  new Label(panel, "Exposure: ", "sans-bold");
171  m_slider = new Slider(panel);
172  m_slider->set_value(0.5f);
173  m_slider->set_fixed_width(120);
174  m_slider->set_callback(
175  [&](float value) {
176  m_scale = std::pow(2.f, (value - 0.5f) * 20);
177  m_render_canvas->set_scale(m_scale);
178  }
179  );
180 
181  Button* buttonSave = new Button(panel, "", FA_SAVE);
182  buttonSave->set_callback(
183  [this]() {
184  using FileType = std::pair<std::string, std::string>;
185  std::vector<FileType> filetypes = { FileType("", "Image File Name") };
186  std::string filename;
187  // Keep asking for a filename until the user cancels or the file does not exist
188  auto file_exists = [](const std::string& name) {
189  std::ifstream f(name.c_str());
190  return f.good();
191  };
192  while (true) {
193  filename = nanogui::file_dialog(filetypes, true);
194  if (filename.empty() || (!file_exists(filename + ".png") && !file_exists(filename + ".exr")))
195  break;
196  cerr << "Error: file \"" << filename << "\" already exists!" << endl;
197  }
198  if (!filename.empty()) {
199  m_block.lock();
200  std::unique_ptr<Bitmap> bitmap(m_block.toBitmap());
201  m_block.unlock();
202  bitmap->array() *= m_scale; // apply exposure
203  bitmap->save(filename);
204  }
205  }
206  );
207  buttonSave->set_tooltip("Save rendering with the selected exposure to disk");
208 
209  set_resize_callback([this](nanogui::Vector2i) { requestLayoutUpdate(); });
210 
211  m_render_canvas = new NoriCanvas(this, m_block);
212  m_render_canvas->set_background_color({ 100, 100, 100, 255 });
213 
214  updateLayout();
215 
216  set_visible(true);
217  draw_all();
218 }
219 
220 
221 void NoriScreen::draw_contents() {
222  if (m_requiresLayoutUpdate) {
223  updateLayout();
224  m_requiresLayoutUpdate = false;
225  }
226 
227  if (m_progressBar) {
228  m_progressBar->set_value(m_renderThread.getProgress());
229  }
230  nanogui::Screen::draw_contents();
231 }
232 
233 
234 void NoriScreen::updateLayout() {
235  perform_layout();
236 
237  {
238  panel->set_position(nanogui::Vector2i((m_size.x() - panel->size().x()) / 2, 0));
239  }
240 
241  nanogui::Vector2i contentOffset(0, PANEL_HEIGHT);
242 
243  {
244  nanogui::Vector2i contentWindow = m_size - contentOffset;
245  nanogui::Vector2f blockSize(m_block.getSize().x(), m_block.getSize().y());
246  nanogui::Vector2f ratio = (blockSize / nanogui::Vector2f(contentWindow));
247  float maxRatio = std::max(ratio.x(), ratio.y());
248  nanogui::Vector2i canvasSize = blockSize / maxRatio;
249 
250  m_render_canvas->set_fixed_size(canvasSize);
251  m_render_canvas->set_position({ ((contentWindow - canvasSize) / 2 + contentOffset).x(), PANEL_HEIGHT });
252  }
253 
254  perform_layout();
255 }
256 
257 
258 bool NoriScreen::drop_event(const std::vector<std::string>& filenames) {
259  if (filenames.size() > 0) {
260  std::string filename = filenames[0];
261  if (filename.size() == 0) {
262  return true;
263  }
264  filesystem::path path(filename);
265 
266  if (path.extension() == "xml") {
267  /* Render the XML scene file */
268  openXML(filename);
269  } else if (path.extension() == "exr") {
270  /* Alternatively, provide a basic OpenEXR image viewer */
271  openEXR(filename);
272  } else {
273  cerr << "Error: unknown file \"" << filename
274  << "\", expected an extension of type .xml or .exr" << endl;
275  }
276 
277  }
278  return true;
279 }
280 
281 bool NoriScreen::keyboard_event(int key, int scancode, int action, int modifiers) {
282  const bool press = (action == GLFW_PRESS);
283  if (press && key == GLFW_KEY_O && modifiers & GLFW_MOD_CONTROL) {
284  using FileType = std::pair<std::string, std::string>;
285  std::vector<FileType> filetypes = { FileType("xml", "Nori Scene File"), FileType("exr", "EXR Image File") };
286  std::string filename = nanogui::file_dialog(filetypes, false);
287  drop_event({ filename });
288  return true;
289  }
290  if (press && key == GLFW_KEY_Z && modifiers & GLFW_MOD_CONTROL) {
291  m_renderThread.stopRendering();
292  return true;
293  }
294 
295  return nanogui::Screen::keyboard_event(key, scancode, action, modifiers);
296 }
297 
298 void NoriScreen::openXML(const std::string& filename) {
299 
300  if(m_renderThread.isBusy()) {
301  cerr << "Error: rendering in progress, you need to wait until it's done" << endl;
302  return;
303  }
304 
305  try {
306 
307  m_renderThread.renderScene(filename);
308 
309  m_block.lock();
310  Vector2i bsize = m_block.getSize();
311  m_render_canvas->update();
312  m_block.unlock();
313 
314  requestLayoutUpdate();
315 
316  } catch (const std::exception &e) {
317  cerr << "Fatal error: " << e.what() << endl;
318  }
319 
320 }
321 
322 void NoriScreen::openEXR(const std::string& filename) {
323 
324  if(m_renderThread.isBusy()) {
325  cerr << "Error: rendering in progress, you need to wait until it's done" << endl;
326  return;
327  }
328 
329  Bitmap bitmap(filename);
330  m_block.lock();
331  m_block.init(Vector2i(bitmap.cols(), bitmap.rows()), nullptr);
332  m_block.fromBitmap(bitmap);
333  Vector2i bsize = m_block.getSize();
334  m_render_canvas->update();
335  m_block.unlock();
336 
337  requestLayoutUpdate();
338 }
339 
340 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