From 83519f0e2a1295d1a89ccfdf10780fd0e30fb6c4 Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 18 Jan 2016 15:58:22 +0530 Subject: [PATCH 01/61] Reorganized code to enable multiple plot rendering In Progress: * Chart class porting Finished: * GLSL shader to std::string headers * Ported Image class to new framework, image examples working fine --- CMakeLists.txt | 2 + CMakeModules/GLSLtoH.cmake | 61 +++++ CMakeModules/glsl2cpp.cpp | 184 ++++++++++++++ examples/cpu/histogram.cpp | 19 +- examples/cpu/plot3.cpp | 19 +- examples/cpu/plotting.cpp | 32 +-- examples/cpu/surface.cpp | 27 +- examples/cuda/fractal.cu | 4 +- examples/cuda/histogram.cu | 19 +- examples/cuda/plot3.cu | 19 +- examples/cuda/plotting.cu | 30 +-- examples/cuda/surface.cu | 24 +- examples/opencl/histogram.cpp | 25 +- examples/opencl/plot3.cpp | 25 +- examples/opencl/plotting.cpp | 30 +-- examples/opencl/surface.cpp | 25 +- include/CPUCopy.hpp | 8 +- include/CUDACopy.hpp | 6 +- include/OpenCLCopy.hpp | 4 +- include/fg/chart.h | 112 +++++++++ include/fg/defines.h | 31 ++- include/fg/histogram.h | 72 +++--- include/fg/image.h | 48 +++- include/fg/plot.h | 70 +++--- include/fg/plot3.h | 155 ------------ include/fg/surface.h | 87 +++---- include/fg/window.h | 107 +------- include/forge.h | 1 - src/CMakeLists.txt | 21 ++ src/chart.cpp | 321 +++++++++++++++++------- src/chart.hpp | 176 +++++++------ src/common.cpp | 176 +++++++------ src/common.hpp | 221 ++++++++++++---- src/exception.cpp | 2 +- src/font.cpp | 34 +-- src/histogram.cpp | 255 ++++++++----------- src/histogram.hpp | 85 ++++--- src/image.cpp | 268 +++++++++----------- src/image.hpp | 86 ++++--- src/plot.cpp | 297 +++------------------- src/plot.hpp | 341 +++++++++++++++++++++---- src/plot3.cpp | 354 -------------------------- src/plot3.hpp | 126 ---------- src/shaders/chart_fs.glsl | 10 + src/shaders/chart_vs.glsl | 10 + src/shaders/font_fs.glsl | 14 ++ src/shaders/font_vs.glsl | 15 ++ src/shaders/histogram_fs.glsl | 12 + src/shaders/histogram_vs.glsl | 29 +++ src/shaders/image_fs.glsl | 31 +++ src/shaders/image_vs.glsl | 14 ++ src/shaders/marker2d_vs.glsl | 16 ++ src/shaders/marker_fs.glsl | 49 ++++ src/shaders/plot3_fs.glsl | 29 +++ src/shaders/plot3_vs.glsl | 19 ++ src/shaders/tick_fs.glsl | 16 ++ src/surface.cpp | 458 ++++++++++++++-------------------- src/surface.hpp | 130 +++++----- src/window.cpp | 92 +++---- src/window.hpp | 70 +++--- 60 files changed, 2494 insertions(+), 2529 deletions(-) create mode 100644 CMakeModules/GLSLtoH.cmake create mode 100644 CMakeModules/glsl2cpp.cpp create mode 100644 include/fg/chart.h delete mode 100644 include/fg/plot3.h delete mode 100644 src/plot3.cpp delete mode 100644 src/plot3.hpp create mode 100644 src/shaders/chart_fs.glsl create mode 100644 src/shaders/chart_vs.glsl create mode 100644 src/shaders/font_fs.glsl create mode 100644 src/shaders/font_vs.glsl create mode 100644 src/shaders/histogram_fs.glsl create mode 100644 src/shaders/histogram_vs.glsl create mode 100644 src/shaders/image_fs.glsl create mode 100644 src/shaders/image_vs.glsl create mode 100644 src/shaders/marker2d_vs.glsl create mode 100644 src/shaders/marker_fs.glsl create mode 100644 src/shaders/plot3_fs.glsl create mode 100644 src/shaders/plot3_vs.glsl create mode 100644 src/shaders/tick_fs.glsl diff --git a/CMakeLists.txt b/CMakeLists.txt index 353bdf45..4390d18a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,8 @@ IF(APPLE) SET(X11_LIBS ${X11_LIBRARIES}) ENDIF(APPLE) +ADD_EXECUTABLE(glsl2cpp "${CMAKE_MODULE_PATH}/glsl2cpp.cpp") + ADD_SUBDIRECTORY(src) IF(BUILD_EXAMPLES) diff --git a/CMakeModules/GLSLtoH.cmake b/CMakeModules/GLSLtoH.cmake new file mode 100644 index 00000000..48bf1069 --- /dev/null +++ b/CMakeModules/GLSLtoH.cmake @@ -0,0 +1,61 @@ +# Function to turn an GLSL shader source file into a C string within a source file. +# xxd uses its input's filename to name the string and its length, so we +# need to move them to a name that depends only on the path output, not its +# input. Otherwise, builds in different relative locations would put the +# source into different variable names, and everything would fall over. +# The actual name will be filename (.s replaced with underscores), and length +# name_len. +# +# Usage example: +# +# set(KERNELS a.cl b/c.cl) +# resource_to_cxx_source( +# SOURCES ${KERNELS} +# VARNAME OUTPUTS +# ) +# add_executable(foo ${OUTPUTS}) +# +# The namespace they are placed in is taken from filename.namespace. +# +# For example, if the input file is kernel.cl, the two variables will be +# unsigned char ns::kernel_cl[]; +# unsigned int ns::kernel_cl_len; +# +# where ns is the contents of kernel.cl.namespace. + +INCLUDE(CMakeParseArguments) + +SET(GLSL2CPP_PROGRAM "glsl2cpp") + +FUNCTION(GLSL_TO_H) + CMAKE_PARSE_ARGUMENTS(RTCS "" "VARNAME;EXTENSION;OUTPUT_DIR;TARGETS;NAMESPACE;EOF" "SOURCES" ${ARGN}) + + SET(_output_files "") + FOREACH(_input_file ${RTCS_SOURCES}) + GET_FILENAME_COMPONENT(_path "${_input_file}" PATH) + GET_FILENAME_COMPONENT(_name "${_input_file}" NAME) + GET_FILENAME_COMPONENT(var_name "${_input_file}" NAME_WE) + + SET(_namespace "${RTCS_NAMESPACE}") + STRING(REPLACE "." "_" var_name ${var_name}) + + SET(_output_path "${CMAKE_CURRENT_BINARY_DIR}/${RTCS_OUTPUT_DIR}") + SET(_output_file "${_output_path}/${var_name}.${RTCS_EXTENSION}") + + ADD_CUSTOM_COMMAND( + OUTPUT ${_output_file} + DEPENDS ${_input_file} ${GLSL2CPP_PROGRAM} + COMMAND ${CMAKE_COMMAND} -E make_directory "${_output_path}" + COMMAND ${CMAKE_COMMAND} -E echo "\\#include \\<${_path}/${var_name}.hpp\\>" >>"${_output_file}" + COMMAND ${GLSL2CPP_PROGRAM} --file ${_name} --namespace ${_namespace} --output ${_output_file} --name ${var_name} --eof ${RTCS_EOF} + WORKING_DIRECTORY "${_path}" + COMMENT "Converting ${_input_file} to GLSL source string" + ) + + LIST(APPEND _output_files ${_output_file}) + ENDFOREACH() + ADD_CUSTOM_TARGET(${RTCS_NAMESPACE}_bin_target DEPENDS ${_output_files}) + + SET("${RTCS_VARNAME}" ${_output_files} PARENT_SCOPE) + SET("${RTCS_TARGETS}" ${RTCS_NAMESPACE}_bin_target PARENT_SCOPE) +ENDFUNCTION(GLSL_TO_H) diff --git a/CMakeModules/glsl2cpp.cpp b/CMakeModules/glsl2cpp.cpp new file mode 100644 index 00000000..e50e2540 --- /dev/null +++ b/CMakeModules/glsl2cpp.cpp @@ -0,0 +1,184 @@ +// Umar Arshad +// Copyright 2015-2019 +// +// Modified by Pradeep Garigipati on Dec 30, 2015 for Forge +// Purpose of modification: To use the program to convert +// GLSL shader files into compile time constant string literals + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +typedef map opt_t; + +static +void print_usage() { + cout << R"delimiter(GLSL2CPP +Converts OpenGL shader files to C++ headers. It is similar to bin2c and xxd but adds +support for namespaces. + +| --name | name of the variable (default: var) | +| --file | input file | +| --output | output file (If no output is specified then it prints to stdout) | +| --type | Type of variable (default: char) | +| --namespace | A space seperated list of namespaces | +| --formatted | Tabs for formatting | +| --version | Prints my name | +| --help | Prints usage info | + +Example +------- +Command: +./glsl2cpp --file blah.txt --namespace shaders --formatted --name image_vs + +Will produce the following: +#pragma once +#include +namespace shaders { + static const char image_vs[] = R"shader( +#version 330 + +layout(location = 0) in vec3 pos; +layout(location = 1) in vec2 tex; + +uniform mat4 matrix; + +out vec2 texcoord; + +void main() { + texcoord = tex; + gl_Position = matrix * vec4(pos,1.0); +} + )shader"; +} +)delimiter"; + exit(0); +} + +static bool formatted; + +static +void add_tabs(const int level) +{ + if(formatted) { + for(int i =0; i < level; i++) { + cout << "\t"; + } + } +} + +static +opt_t parse_options(const vector& args) +{ + opt_t options; + + options["--name"] = ""; + options["--type"] = ""; + options["--file"] = ""; + options["--output"] = ""; + options["--namespace"] = ""; + options["--eof"] = ""; + + //Parse Arguments + string curr_opt; + bool verbose = false; + for(auto arg : args) { + if(arg == "--verbose") { + verbose = true; + } else if(arg == "--formatted") { + formatted = true; + } else if(arg == "--version") { + cout << args[0] << " Original Author: Umar Arshad;\n Modified later by: Pradeep Garigipati." << endl; + } else if(arg == "--help") { + print_usage(); + } else if(options.find(arg) != options.end()) { + curr_opt = arg; + } else if(curr_opt.empty()) { + //cerr << "Invalid Argument: " << arg << endl; + } else { + if(options[curr_opt] != "") { + options[curr_opt] += " " + arg; + } + else { + options[curr_opt] += arg; + } + } + } + + if(verbose) { + for(auto opts : options) { + cout << get<0>(opts) << " " << get<1>(opts) << endl; + } + } + return options; +} + +int main(int argc, const char * const * const argv) +{ + + vector args(argv, argv+argc); + + opt_t&& options = parse_options(args); + + //Save default cout buffer. Need this to prevent crash. + auto bak = cout.rdbuf(); + unique_ptr outfile; + + // Set defaults + if(options["--name"] == "") { options["--name"] = "var"; } + if(options["--output"] != "") { + //redirect stream if output file is specified + outfile.reset(new ofstream(options["--output"])); + cout.rdbuf(outfile->rdbuf()); + } + + cout << "#pragma once\n"; + cout << "#include \n"; // defines std::string + + int ns_cnt = 0; + int level = 0; + if(options["--namespace"] != "") { + std::stringstream namespaces(options["--namespace"]); + string name; + namespaces >> name; + do { + add_tabs(level++); + cout << "namespace " << name << "\n{\n"; + ns_cnt++; + namespaces >> name; + } while(!namespaces.fail()); + } + + if(options["--type"] == "") { + options["--type"] = "std::string"; + } + add_tabs(level); + cout << "static const " << options["--type"] << " " << options["--name"] << " = R\"shader(\n"; + level++; + + ifstream input(options["--file"]); + + for(std::string line; std::getline(input, line);) { + add_tabs(level); + cout << line << endl; + } + + if (options["--eof"].c_str()[0] == '1') { + // Add end of file character + cout << "0x0"; + } + + add_tabs(--level); + cout << ")shader\";\n"; + + while(ns_cnt--) { + add_tabs(--level); + cout << "}\n"; + } + cout.rdbuf(bak); +} diff --git a/examples/cpu/histogram.cpp b/examples/cpu/histogram.cpp index aef9f7e1..b08cee44 100644 --- a/examples/cpu/histogram.cpp +++ b/examples/cpu/histogram.cpp @@ -67,15 +67,20 @@ int main(void) { wnd.grid(WIN_ROWS, WIN_COLS); fg::Image img(DIMX, DIMY, fg::FG_RGBA, fg::u8); + + fg::Chart chart(fg::FG_2D); + /* set x axis limits to maximum and minimum values of data + * and y axis limits to range [0, nBins]*/ + chart.setAxesLimits(0, 1, 0, 1000); + /* - * Create histogram object while specifying desired number of bins + * Create histogram object specifying number of bins */ - fg::Histogram hist(NBINS, fg::u8); - + fg::Histogram hist = chart.histogram(NBINS, fg::u8); /* * Set histogram colors */ - hist.setBarColor(fg::FG_YELLOW); + hist.setColor(fg::FG_YELLOW); /* * generate image, and prepare data to pass into @@ -84,10 +89,6 @@ int main(void) { kernel(bmp); fg::copy(img, bmp.ptr); - /* set x axis limits to maximum and minimum values of data - * and y axis limits to range [0, nBins]*/ - hist.setAxesLimits(1, 0, 1000, 0); - /* copy your data into the vertex buffer object exposed by * fg::Histogram class and then proceed to rendering. * To help the users with copying the data from compute @@ -109,7 +110,7 @@ int main(void) { fg::copy(hist, histogram_array); wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); - wnd.draw(1, 0, hist, "Histogram of Noisy Image"); + wnd.draw(1, 0, chart, "Histogram of Noisy Image"); // draw window and poll for events last wnd.swapBuffers(); } while(!wnd.close()); diff --git a/examples/cpu/plot3.cpp b/examples/cpu/plot3.cpp index 0666e8a0..de982908 100644 --- a/examples/cpu/plot3.cpp +++ b/examples/cpu/plot3.cpp @@ -52,20 +52,11 @@ int main(void){ #endif wnd.setFont(&fnt); - /* Create several plot objects which creates the necessary - * vertex buffer objects to hold the different plot types - */ - fg::Plot3 plot3(ZSIZE, fg::f32); + fg::Chart chart(fg::FG_3D); + chart.setAxesLimits(-1.1f, 1.1f, -1.1f, 1.1f, 0.f, 10.f); + chart.setAxesTitles("x-axis", "y-axis", "z-axis"); - /* - * Set draw limits for plots - */ - plot3.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, 0.f); - - /* - * Set axis titles - */ - plot3.setAxesTitles("x-axis", "y-axis", "z-axis"); + fg::Plot plot3 = chart.plot(ZSIZE, fg::f32); //generate a surface std::vector function; @@ -84,7 +75,7 @@ int main(void){ gen_curve(t, DX, function); copy(plot3, &function[0]); // draw window and poll for events last - wnd.draw(plot3); + wnd.draw(chart); } while(!wnd.close()); return 0; diff --git a/examples/cpu/plotting.cpp b/examples/cpu/plotting.cpp index cbc7daee..03d3e605 100644 --- a/examples/cpu/plotting.cpp +++ b/examples/cpu/plotting.cpp @@ -16,8 +16,6 @@ const unsigned DIMX = 1000; const unsigned DIMY = 800; -const unsigned WIN_ROWS = 2; -const unsigned WIN_COLS = 2; const float FRANGE_START = 0.f; const float FRANGE_END = 2.f * 3.1415926f; @@ -31,7 +29,8 @@ void map_range_to_vec_vbo(float range_start, float range_end, float dx, std::vec } } -int main(void){ +int main(void) +{ std::vector function; map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, function, &sinf); @@ -53,18 +52,16 @@ int main(void){ #endif wnd.setFont(&fnt); - /* - * Split the window into grid regions - */ - wnd.grid(WIN_ROWS, WIN_COLS); + fg::Chart chart(fg::FG_2D); + chart.setAxesLimits(FRANGE_START, FRANGE_END, -1.1f, 1.1f); /* Create several plot objects which creates the necessary * vertex buffer objects to hold the different plot types */ - fg::Plot plt0(function.size()/2, fg::f32); //create a default plot - fg::Plot plt1(function.size()/2, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type - fg::Plot plt2(function.size()/2, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape - fg::Plot plt3(function.size()/2, fg::f32, fg::FG_SCATTER, fg::FG_POINT); + fg::Plot plt0 = chart.plot(function.size()/2, fg::f32); //create a default plot + fg::Plot plt1 = chart.plot(function.size()/2, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type + fg::Plot plt2 = chart.plot(function.size()/2, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape + fg::Plot plt3 = chart.plot(function.size()/2, fg::f32, fg::FG_SCATTER, fg::FG_POINT); /* * Set plot colors @@ -74,13 +71,6 @@ int main(void){ plt2.setColor(fg::FG_WHITE); //use a forge predefined color plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color - /* - * Set draw limits for plots - */ - plt0.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt1.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt2.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt3.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); /* copy your data into the pixel buffer object exposed by * fg::Plot class and then proceed to rendering. @@ -94,11 +84,7 @@ int main(void){ copy(plt3, &function[0]); do { - wnd.draw(0, 0, plt0, NULL ); - wnd.draw(0, 1, plt1, "sinf_line_blue" ); - wnd.draw(1, 1, plt2, "sinf_line_triangle" ); - wnd.draw(1, 0, plt3, "sinf_scatter_point" ); - // draw window and poll for events last + wnd.draw(chart); wnd.swapBuffers(); } while(!wnd.close()); diff --git a/examples/cpu/surface.cpp b/examples/cpu/surface.cpp index 11b13bce..88921676 100644 --- a/examples/cpu/surface.cpp +++ b/examples/cpu/surface.cpp @@ -39,7 +39,8 @@ void gen_surface(float t, float dx, std::vector &vec ){ } } -int main(void){ +int main(void) +{ /* * First Forge call should be a window creation call * so that necessary OpenGL context is created for any @@ -58,26 +59,13 @@ int main(void){ #endif wnd.setFont(&fnt); - /* Create several plot objects which creates the necessary - * vertex buffer objects to hold the different plot types - */ - fg::Surface surf(XSIZE, YSIZE, fg::f32, fg::FG_SURFACE); + fg::Chart chart(fg::FG_3D); + chart.setAxesLimits(-1.1f, 1.1f, -1.1f, 1.1f, -5.f, 10.f); + chart.setAxesTitles("x-axis", "y-axis", "z-axis"); - /* - * Set plot colors - */ + fg::Surface surf = chart.surface(XSIZE, YSIZE, fg::f32); surf.setColor(fg::FG_YELLOW); - /* - * Set draw limits for plots - */ - surf.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, -5.f); - - /* - * Set axis titles - */ - surf.setAxesTitles("x-axis", "y-axis", "z-axis"); - //generate a surface std::vector function; static float t=0; @@ -94,8 +82,7 @@ int main(void){ t+=0.07; gen_surface(t, DX, function); copy(surf, &function[0]); - // draw window and poll for events last - wnd.draw(surf); + wnd.draw(chart); } while(!wnd.close()); return 0; diff --git a/examples/cuda/fractal.cu b/examples/cuda/fractal.cu index 0b18d926..9e8e46c1 100644 --- a/examples/cuda/fractal.cu +++ b/examples/cuda/fractal.cu @@ -85,9 +85,9 @@ void julia(unsigned char* out) // now calculate the value at that position int juliaValue = julia(x, y); - out[offset*4 + 0] = 255 * juliaValue; + out[offset*4 + 2] = 255 * juliaValue; + out[offset*4 + 0] = 0; out[offset*4 + 1] = 0; - out[offset*4 + 2] = 0; out[offset*4 + 3] = 255; } } diff --git a/examples/cuda/histogram.cu b/examples/cuda/histogram.cu index 652a5c33..268c08fe 100644 --- a/examples/cuda/histogram.cu +++ b/examples/cuda/histogram.cu @@ -75,19 +75,21 @@ int main(void) wnd.grid(WIN_ROWS, WIN_COLS); fg::Image img(DIMX, DIMY, fg::FG_RGBA, fg::u8); + + fg::Chart chart(fg::FG_2D); + /* set x axis limits to maximum and minimum values of data + * and y axis limits to range [0, nBins]*/ + chart.setAxesLimits(0, 1, 0, 1000); + /* - * Create histogram object while specifying desired number of bins + * Create histogram object specifying number of bins */ - fg::Histogram hist(NBINS, fg::u8); - + fg::Histogram hist = chart.histogram(NBINS, fg::u8); /* * Set histogram colors */ - hist.setBarColor(fg::FG_YELLOW); + hist.setColor(fg::FG_YELLOW); - /* set x axis limits to maximum and minimum values of data - * and y axis limits to range [0, nBins]*/ - hist.setAxesLimits(1, 0, 1000, 0); CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, IMG_SIZE )); CUDA_ERROR_CHECK(cudaMalloc((void**)&hist_out, NBINS * sizeof(int))); kernel(dev_out); @@ -102,8 +104,9 @@ int main(void) // limit histogram update frequency if(fmod(persistance, 0.5f) < 0.01) fg::copy(hist, hist_out); + wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); - wnd.draw(1, 0, hist, "Histogram of Noisy Image"); + wnd.draw(1, 0, chart, "Histogram of Noisy Image"); wnd.swapBuffers(); } while(!wnd.close()); diff --git a/examples/cuda/plot3.cu b/examples/cuda/plot3.cu index 2b40682f..fd5a7d82 100644 --- a/examples/cuda/plot3.cu +++ b/examples/cuda/plot3.cu @@ -47,20 +47,11 @@ int main(void) #endif wnd.setFont(&fnt); - /* Create several plot objects which creates the necessary - * vertex buffer objects to hold the different plot types - */ - fg::Plot3 plot3(ZSIZE, fg::f32); + fg::Chart chart(fg::FG_3D); + chart.setAxesLimits(-1.1f, 1.1f, -1.1f, 1.1f, 0.f, 10.f); + chart.setAxesTitles("x-axis", "y-axis", "z-axis"); - /* - * Set draw limits for plots - */ - plot3.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, 0.f); - - /* - * Set axis titles - */ - plot3.setAxesTitles("x-axis", "y-axis", "z-axis"); + fg::Plot plot3 = chart.plot(ZSIZE, fg::f32); static float t=0; CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, ZSIZE * 3 * sizeof(float) )); @@ -79,7 +70,7 @@ int main(void) kernel(t, DX, dev_out); fg::copy(plot3, dev_out); // draw window and poll for events last - wnd.draw(plot3); + wnd.draw(chart); } while(!wnd.close()); CUDA_ERROR_CHECK(cudaFree(dev_out)); diff --git a/examples/cuda/plotting.cu b/examples/cuda/plotting.cu index 40ba9050..c1706d43 100644 --- a/examples/cuda/plotting.cu +++ b/examples/cuda/plotting.cu @@ -7,8 +7,6 @@ const unsigned DIMX = 1000; const unsigned DIMY = 800; -const unsigned WIN_ROWS = 2; -const unsigned WIN_COLS = 2; static const float dx = 0.1; static const float FRANGE_START = 0.f; @@ -40,18 +38,16 @@ int main(void) #endif wnd.setFont(&fnt); - /* - * Split the window into grid regions - */ - wnd.grid(WIN_ROWS, WIN_COLS); + fg::Chart chart(fg::FG_2D); + chart.setAxesLimits(FRANGE_START, FRANGE_END, -1.1f, 1.1f); /* Create several plot objects which creates the necessary * vertex buffer objects to hold the different plot types */ - fg::Plot plt0( DATA_SIZE, fg::f32); //create a default plot - fg::Plot plt1( DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type - fg::Plot plt2( DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape - fg::Plot plt3( DATA_SIZE, fg::f32, fg::FG_SCATTER, fg::FG_POINT); + fg::Plot plt0 = chart.plot( DATA_SIZE, fg::f32); //create a default plot + fg::Plot plt1 = chart.plot( DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type + fg::Plot plt2 = chart.plot( DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape + fg::Plot plt3 = chart.plot( DATA_SIZE, fg::f32, fg::FG_SCATTER, fg::FG_POINT); /* * Set plot colors @@ -61,14 +57,6 @@ int main(void) plt2.setColor(fg::FG_WHITE); //use a forge predefined color plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color - /* - * Set draw limits for plots - */ - plt0.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt1.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt2.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt3.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, sizeof(float) * DATA_SIZE * 2)); kernel(dev_out); /* copy your data into the vertex buffer object exposed by @@ -83,11 +71,7 @@ int main(void) fg::copy(plt3, dev_out); do { - wnd.draw(0, 0, plt0, NULL ); - wnd.draw(0, 1, plt1, "sinf_line_blue" ); - wnd.draw(1, 1, plt2, "sinf_line_triangle" ); - wnd.draw(1, 0, plt3, "sinf_scatter_point" ); - // draw window and poll for events last + wnd.draw(chart); wnd.swapBuffers(); } while(!wnd.close()); diff --git a/examples/cuda/surface.cu b/examples/cuda/surface.cu index 4ea89d03..fc21bfe9 100644 --- a/examples/cuda/surface.cu +++ b/examples/cuda/surface.cu @@ -41,26 +41,13 @@ int main(void) #endif wnd.setFont(&fnt); - /* Create several plot objects which creates the necessary - * vertex buffer objects to hold the different plot types - */ - fg::Surface surf(XSIZE, YSIZE, fg::f32, fg::FG_SURFACE); + fg::Chart chart(fg::FG_3D); + chart.setAxesLimits(-1.1f, 1.1f, -1.1f, 1.1f, -5.f, 10.f); + chart.setAxesTitles("x-axis", "y-axis", "z-axis"); - /* - * Set plot colors - */ + fg::Surface surf = chart.surface(XSIZE, YSIZE, fg::f32); surf.setColor(fg::FG_YELLOW); - /* - * Set draw limits for plots - */ - surf.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, -5.f); - - /* - * Set axis titles - */ - surf.setAxesTitles("x-axis", "y-axis", "z-axis"); - static float t=0; CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, XSIZE * YSIZE * 3 * sizeof(float) )); kernel(t, DX, dev_out); @@ -76,8 +63,7 @@ int main(void) t+=0.07; kernel(t, DX, dev_out); fg::copy(surf, dev_out); - // draw window and poll for events last - wnd.draw(surf); + wnd.draw(chart); } while(!wnd.close()); CUDA_ERROR_CHECK(cudaFree(dev_out)); diff --git a/examples/opencl/histogram.cpp b/examples/opencl/histogram.cpp index 9fc85ff5..f897aaa7 100644 --- a/examples/opencl/histogram.cpp +++ b/examples/opencl/histogram.cpp @@ -205,23 +205,21 @@ int main(void) */ wnd.grid(WIN_ROWS, WIN_COLS); - /* Create an image object which creates the necessary - * textures and pixel buffer objects to hold the image - * */ fg::Image img(DIMX, DIMY, fg::FG_RGBA, fg::u8); + + fg::Chart chart(fg::FG_2D); + /* set x axis limits to maximum and minimum values of data + * and y axis limits to range [0, nBins]*/ + chart.setAxesLimits(0, 1, 0, 1000); + /* - * Create histogram object while specifying desired number of bins + * Create histogram object specifying number of bins */ - fg::Histogram hist(NBINS, fg::u8); - + fg::Histogram hist = chart.histogram(NBINS, fg::u8); /* * Set histogram colors */ - hist.setBarColor(fg::FG_YELLOW); - - /* set x axis limits to maximum and minimum values of data - * and y axis limits to range [0, nBins]*/ - hist.setAxesLimits(1, 0, 1000, 0); + hist.setColor(fg::FG_YELLOW); Platform plat = getPlatform(); // Select the default platform and create a context using this platform and the GPU @@ -285,12 +283,15 @@ int main(void) do { kernel(devOut, histOut, queue); fg::copy(img, devOut, queue); + // limit histogram update frequency if(fmod(persistance, 0.4f) < 0.02f) fg::copy(hist, histOut, queue); + // draw window and poll for events last wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); - wnd.draw(1, 0, hist, "Histogram of Noisy Image"); + wnd.draw(1, 0, chart, "Histogram of Noisy Image"); + wnd.swapBuffers(); } while(!wnd.close()); }catch (fg::Error err) { diff --git a/examples/opencl/plot3.cpp b/examples/opencl/plot3.cpp index d1404ccc..232b0e7b 100644 --- a/examples/opencl/plot3.cpp +++ b/examples/opencl/plot3.cpp @@ -93,26 +93,11 @@ int main(void) #endif wnd.setFont(&fnt); - /* Create several plot objects which creates the necessary - * vertex buffer objects to hold the different plot types - */ - fg::Plot3 plot3(ZSIZE, fg::f32); - - /* - * Set draw limits for plots - */ - plot3.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, 0.f); - - /* - * Set draw limits for plots - */ - plot3.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, 0.f); - - /* - * Set axis titles - */ - plot3.setAxesTitles("x-axis", "y-axis", "z-axis"); + fg::Chart chart(fg::FG_3D); + chart.setAxesLimits(-1.1f, 1.1f, -1.1f, 1.1f, 0.f, 10.f); + chart.setAxesTitles("x-axis", "y-axis", "z-axis"); + fg::Plot plot3 = chart.plot(ZSIZE, fg::f32); Platform plat = getPlatform(); // Select the default platform and create a context using this platform and the GPU @@ -174,7 +159,7 @@ int main(void) kernel(devOut, queue, t); fg::copy(plot3, devOut, queue); // draw window and poll for events last - wnd.draw(plot3); + wnd.draw(chart); } while(!wnd.close()); }catch (fg::Error err) { std::cout << err.what() << "(" << err.err() << ")" << std::endl; diff --git a/examples/opencl/plotting.cpp b/examples/opencl/plotting.cpp index ba7f8af0..fac7599d 100644 --- a/examples/opencl/plotting.cpp +++ b/examples/opencl/plotting.cpp @@ -24,8 +24,6 @@ using namespace std; const unsigned DIMX = 1000; const unsigned DIMY = 800; -const unsigned WIN_ROWS = 2; -const unsigned WIN_COLS = 2; const float dx = 0.1; const float FRANGE_START = 0.f; @@ -83,18 +81,16 @@ int main(void) #endif wnd.setFont(&fnt); - /* - * Split the window into grid regions - */ - wnd.grid(WIN_ROWS, WIN_COLS); + fg::Chart chart(fg::FG_2D); + chart.setAxesLimits(FRANGE_START, FRANGE_END, -1.1f, 1.1f); /* Create several plot objects which creates the necessary * vertex buffer objects to hold the different plot types */ - fg::Plot plt0(DATA_SIZE, fg::f32); //create a default plot - fg::Plot plt1(DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type - fg::Plot plt2(DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape - fg::Plot plt3(DATA_SIZE, fg::f32, fg::FG_SCATTER, fg::FG_POINT); + fg::Plot plt0 = chart.plot(DATA_SIZE, fg::f32); //create a default plot + fg::Plot plt1 = chart.plot(DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type + fg::Plot plt2 = chart.plot(DATA_SIZE, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape + fg::Plot plt3 = chart.plot(DATA_SIZE, fg::f32, fg::FG_SCATTER, fg::FG_POINT); /* * Set plot colors @@ -104,14 +100,6 @@ int main(void) plt2.setColor(fg::FG_WHITE); //use a forge predefined color plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color - /* - * Set draw limits for plots - */ - plt0.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt1.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt2.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - plt3.setAxesLimits(FRANGE_END, FRANGE_START, 1.1f, -1.1f); - Platform plat = getPlatform(); // Select the default platform and create a context using this platform and the GPU #if defined(OS_MAC) @@ -171,11 +159,7 @@ int main(void) fg::copy(plt3, devOut, queue); do { - wnd.draw(0, 0, plt0, NULL ); - wnd.draw(0, 1, plt1, "sinf_line_blue" ); - wnd.draw(1, 1, plt2, "sinf_line_triangle" ); - wnd.draw(1, 0, plt3, "sinf_scatter_point" ); - // draw window and poll for events last + wnd.draw(chart); wnd.swapBuffers(); } while(!wnd.close()); }catch (fg::Error err) { diff --git a/examples/opencl/surface.cpp b/examples/opencl/surface.cpp index 9552ee20..d9b16ccf 100644 --- a/examples/opencl/surface.cpp +++ b/examples/opencl/surface.cpp @@ -99,27 +99,13 @@ int main(void) #endif wnd.setFont(&fnt); - /* Create several plot objects which creates the necessary - * vertex buffer objects to hold the different plot types - */ - fg::Surface surf(XSIZE, YSIZE, fg::f32, fg::FG_SURFACE); + fg::Chart chart(fg::FG_3D); + chart.setAxesLimits(-1.1f, 1.1f, -1.1f, 1.1f, -5.f, 10.f); + chart.setAxesTitles("x-axis", "y-axis", "z-axis"); - /* - * Set plot colors - */ + fg::Surface surf = chart.surface(XSIZE, YSIZE, fg::f32); surf.setColor(fg::FG_YELLOW); - /* - * Set draw limits for plots - */ - surf.setAxesLimits(1.1f, -1.1f, 1.1f, -1.1f, 10.f, -5.f); - - /* - * Set axis titles - */ - surf.setAxesTitles("x-axis", "y-axis", "z-axis"); - - Platform plat = getPlatform(); // Select the default platform and create a context using this platform and the GPU #if defined(OS_MAC) @@ -179,8 +165,7 @@ int main(void) t+=0.07; kernel(devOut, queue, t); fg::copy(surf, devOut, queue); - // draw window and poll for events last - wnd.draw(surf); + wnd.draw(chart); } while(!wnd.close()); }catch (fg::Error err) { std::cout << err.what() << "(" << err.err() << ")" << std::endl; diff --git a/include/CPUCopy.hpp b/include/CPUCopy.hpp index 4334bc50..fde8f9a9 100644 --- a/include/CPUCopy.hpp +++ b/include/CPUCopy.hpp @@ -25,16 +25,16 @@ void copy(fg::Image& out, const T * dataPtr) * Below functions takes any renderable forge object that has following member functions * defined * - * `unsigned Renderable::vbo() const;` - * `unsigned Renderable::size() const;` + * `unsigned Renderable::vertices() const;` + * `unsigned Renderable::verticesSize() const;` * * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill */ template void copy(Renderable& out, const T * dataPtr) { - glBindBuffer(GL_ARRAY_BUFFER, out.vbo()); - glBufferSubData(GL_ARRAY_BUFFER, 0, out.size(), dataPtr); + glBindBuffer(GL_ARRAY_BUFFER, out.vertices()); + glBufferSubData(GL_ARRAY_BUFFER, 0, out.verticesSize(), dataPtr); glBindBuffer(GL_ARRAY_BUFFER, 0); } diff --git a/include/CUDACopy.hpp b/include/CUDACopy.hpp index 31b8753a..cf0e0bb8 100644 --- a/include/CUDACopy.hpp +++ b/include/CUDACopy.hpp @@ -46,8 +46,8 @@ void copy(fg::Image& out, const T * devicePtr) * Below functions takes any renderable forge object that has following member functions * defined * - * `unsigned Renderable::vbo() const;` - * `unsigned Renderable::size() const;` + * `unsigned Renderable::vertices() const;` + * `unsigned Renderable::verticesSize() const;` * * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill */ @@ -55,7 +55,7 @@ template void copy(Renderable& out, const T * devicePtr) { cudaGraphicsResource *cudaVBOResource; - CUDA_ERROR_CHECK(cudaGraphicsGLRegisterBuffer(&cudaVBOResource, out.vbo(), cudaGraphicsMapFlagsWriteDiscard)); + CUDA_ERROR_CHECK(cudaGraphicsGLRegisterBuffer(&cudaVBOResource, out.vertices(), cudaGraphicsMapFlagsWriteDiscard)); size_t num_bytes; T* vboDevicePtr = NULL; diff --git a/include/OpenCLCopy.hpp b/include/OpenCLCopy.hpp index 7f0db3a7..55d5228a 100644 --- a/include/OpenCLCopy.hpp +++ b/include/OpenCLCopy.hpp @@ -39,14 +39,14 @@ static void copy(fg::Image& out, const cl::Buffer& in, const cl::CommandQueue& q template void copy(Renderable& out, const cl::Buffer& in, const cl::CommandQueue& queue) { - cl::BufferGL vboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, out.vbo(), NULL); + cl::BufferGL vboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, out.vertices(), NULL); std::vector shared_objects; shared_objects.push_back(vboMapBuffer); glFinish(); queue.enqueueAcquireGLObjects(&shared_objects); - queue.enqueueCopyBuffer(in, vboMapBuffer, 0, 0, out.size(), NULL, NULL); + queue.enqueueCopyBuffer(in, vboMapBuffer, 0, 0, out.verticesSize(), NULL, NULL); queue.finish(); queue.enqueueReleaseGLObjects(&shared_objects); } diff --git a/include/fg/chart.h b/include/fg/chart.h new file mode 100644 index 00000000..d1a95058 --- /dev/null +++ b/include/fg/chart.h @@ -0,0 +1,112 @@ +/******************************************************* + * Copyright (c) 2015-2019, ArrayFire + * All rights reserved. + * + * This file is distributed under 3-clause BSD license. + * The complete license agreement can be obtained at: + * http://arrayfire.com/licenses/BSD-3-Clause + ********************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace internal +{ +class _Chart; +} + +namespace fg +{ + +/** + \class Chart + */ +class Chart { + private: + ChartType mChartType; + internal::_Chart* mValue; + + public: + /** + Creates a Chart object with given dimensional property + + \param[in] pType is chart dimension property + */ + FGAPI Chart(const ChartType cType); + + /** + Chart destructor + */ + FGAPI ~Chart(); + + /** + Set axes titles for the chart + + \param[in] x is x-axis title label + \param[in] y is y-axis title label + \param[in] z is z-axis title label + */ + FGAPI void setAxesTitles(const std::string pX, + const std::string pY, + const std::string pZ=std::string("")); + + /** + Set axes data ranges + + \param[in] xmin is x-axis minimum data value + \param[in] xmax is x-axis maximum data value + \param[in] ymin is y-axis minimum data value + \param[in] ymax is y-axis maximum data value + \param[in] zmin is z-axis minimum data value + \param[in] zmax is z-axis maximum data value + */ + FGAPI void setAxesLimits(const float pXmin, const float pXmax, + const float pYmin, const float pYmax, + const float pZmin=-1, const float pZmax=1); + + FGAPI void add(const Image& pImage); + FGAPI void add(const Histogram& pHistogram); + FGAPI void add(const Plot& pPlot); + FGAPI void add(const Surface& pSurface); + + FGAPI Image image(const uint pWidth, const uint pHeight, + const ChannelFormat pFormat=FG_RGBA, const dtype pDataType=f32); + + FGAPI Histogram histogram(const uint pNBins, const dtype pDataType); + + FGAPI Plot plot(const uint pNumPoints, const dtype pDataType, + const PlotType pPlotType=FG_LINE, const MarkerType pMarkerType=FG_NONE); + + FGAPI Surface surface(const uint pNumXPoints, const uint pNumYPoints, const dtype pDataType, + const PlotType pPlotType=FG_SURFACE, const MarkerType pMarkerType=FG_NONE); + + /** + Render the chart to given window + + \param[in] pWindow is target window to where chart will be rendered + \param[in] pX is x coordinate of origin of viewport in window coordinates + \param[in] pY is y coordinate of origin of viewport in window coordinates + \param[in] pVPW is the width of the viewport + \param[in] pVPH is the height of the viewport + \param[in] pTransform is an array of floats. This vector is expected to contain + at least 16 elements + */ + FGAPI void render(const Window& pWindow, + const int pX, const int pY, const int pVPW, const int pVPH, + const std::vector& pTransform) const; + + /** + Get the handle to internal implementation of Chart + */ + FGAPI internal::_Chart* get() const; +}; + +} diff --git a/include/fg/defines.h b/include/fg/defines.h index aff86208..b9cb394e 100644 --- a/include/fg/defines.h +++ b/include/fg/defines.h @@ -47,6 +47,10 @@ */ FGAPI GLEWContext* glewGetContext(); +typedef unsigned int uint; +typedef unsigned short ushort; +typedef unsigned char uchar; + namespace fg { @@ -107,6 +111,11 @@ enum ChannelFormat { FG_BGRA = 401 ///< Four(Red, Green, Blue & Alpha) channels }; +enum ChartType { + FG_2D = 2, ///< Two dimensional charts + FG_3D = 3 ///< Three dimensional charts +}; + /** Color maps @@ -144,20 +153,20 @@ enum dtype { }; enum PlotType { - FG_LINE = 0, - FG_SCATTER = 1, - FG_SURFACE = 2 + FG_LINE = 0, ///< Line plot + FG_SCATTER = 1, ///< Scatter plot + FG_SURFACE = 2 ///< Surface plot }; enum MarkerType { - FG_NONE = 0, - FG_POINT = 1, - FG_CIRCLE = 2, - FG_SQUARE = 3, - FG_TRIANGLE = 4, - FG_CROSS = 5, - FG_PLUS = 6, - FG_STAR = 7 + FG_NONE = 0, ///< No marker + FG_POINT = 1, ///< Point marker + FG_CIRCLE = 2, ///< Circle marker + FG_SQUARE = 3, ///< Square marker + FG_TRIANGLE = 4, ///< Triangle marker + FG_CROSS = 5, ///< Cross-hair marker + FG_PLUS = 6, ///< Plus symbol marker + FG_STAR = 7 ///< Star symbol marker }; } diff --git a/include/fg/histogram.h b/include/fg/histogram.h index 9e5ffd75..70a7e22a 100644 --- a/include/fg/histogram.h +++ b/include/fg/histogram.h @@ -19,6 +19,8 @@ class _Histogram; namespace fg { +class Window; + /** \class Histogram @@ -26,7 +28,7 @@ namespace fg */ class Histogram { private: - internal::_Histogram* value; + internal::_Histogram* mValue; public: /** @@ -36,14 +38,14 @@ class Histogram { \param[in] pDataType takes one of the values of \ref dtype that indicates the integral data type of histogram data */ - FGAPI Histogram(unsigned pNBins, dtype pDataType); + FGAPI Histogram(const uint pNBins, const dtype pDataType); /** Copy constructor for Histogram \param[in] other is the Histogram of which we make a copy of. */ - FGAPI Histogram(const Histogram& other); + FGAPI Histogram(const Histogram& pOther); /** Histogram Destructor @@ -55,77 +57,73 @@ class Histogram { \param[in] col takes values of type fg::Color to define bar color **/ - FGAPI void setBarColor(fg::Color col); - + FGAPI void setColor(const Color pColor); /** Set the color of bar in the bar graph(histogram) + This is global alpha value for the histogram rendering that takes + effect if individual bar alphas are not set by calling the following + member functions + - Histogram::alphas() + - Histogram::alphasSize() + \param[in] pRed is Red component in range [0, 1] \param[in] pGreen is Green component in range [0, 1] \param[in] pBlue is Blue component in range [0, 1] + \param[in] pAlpha is Alpha component in range [0, 1] */ - FGAPI void setBarColor(float pRed, float pGreen, float pBlue); - - /** - Set the chart axes limits - - \param[in] pXmax is X-Axis maximum value - \param[in] pXmin is X-Axis minimum value - \param[in] pYmax is Y-Axis maximum value - \param[in] pYmin is Y-Axis minimum value - */ - FGAPI void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin); + FGAPI void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha); /** - Set axes titles in histogram(bar chart) + Set legend for histogram plot - \param[in] pXTitle is X-Axis title - \param[in] pYTitle is Y-Axis title + \param[in] pLegend */ - FGAPI void setAxesTitles(const char* pXTitle, const char* pYTitle); + FGAPI void setLegend(const std::string pLegend); /** - Get X-Axis maximum value + Get the OpenGL buffer object identifier for vertices - \return Maximum value along X-Axis + \return OpenGL VBO resource id. */ - FGAPI float xmax() const; + FGAPI uint vertices() const; /** - Get X-Axis minimum value + Get the OpenGL buffer object identifier for color values per vertex - \return Minimum value along X-Axis + \return OpenGL VBO resource id. */ - FGAPI float xmin() const; + FGAPI uint colors() const; /** - Get Y-Axis maximum value + Get the OpenGL buffer object identifier for alpha values per vertex - \return Maximum value along Y-Axis + \return OpenGL VBO resource id. */ - FGAPI float ymax() const; + FGAPI uint alphas() const; /** - Get Y-Axis minimum value + Get the OpenGL Vertex Buffer Object resource size - \return Minimum value along Y-Axis + \return vertex buffer object size in bytes */ - FGAPI float ymin() const; + FGAPI uint verticesSize() const; /** - Get the OpenGL Vertex Buffer Object identifier + Get the OpenGL Vertex Buffer Object resource size - \return OpenGL VBO resource id. + \return colors buffer object size in bytes */ - FGAPI unsigned vbo() const; + FGAPI uint colorsSize() const; /** Get the OpenGL Vertex Buffer Object resource size - \return OpenGL VBO resource size. + \return alpha buffer object size in bytes */ - FGAPI unsigned size() const; + FGAPI uint alphasSize() const; /** Get the handle to internal implementation of Histogram diff --git a/include/fg/image.h b/include/fg/image.h index 3e51a057..bebba56c 100644 --- a/include/fg/image.h +++ b/include/fg/image.h @@ -11,6 +11,8 @@ #include +#include + namespace internal { class _Image; @@ -19,12 +21,14 @@ class _Image; namespace fg { +class Window; + /** \class Image */ class Image { private: - internal::_Image* value; + internal::_Image* mValue; public: /** @@ -37,31 +41,46 @@ class Image { \param[in] pDataType takes one of the values of \ref dtype that indicates the integral data type of histogram data */ - FGAPI Image(unsigned pWidth, unsigned pHeight, ChannelFormat pFormat, dtype pDataType); + FGAPI Image(const uint pWidth, const uint pHeight, + const ChannelFormat pFormat=FG_RGBA, const dtype pDataType=f32); /** Copy constructor of Image \param[in] other is the Image of which we make a copy of. */ - FGAPI Image(const Image& other); + FGAPI Image(const Image& pOther); /** Image Destructor */ FGAPI ~Image(); + /** + Set a global alpha value for rendering the image + + \param[in] pAlpha + */ + FGAPI void setAlpha(const float pAlpha); + + /** + Set option to inform whether to maintain aspect ratio of original image + + \param[in] pKeep + */ + FGAPI void keepAspectRatio(const bool pKeep); + /** Get Image width \return image width */ - FGAPI unsigned width() const; + FGAPI uint width() const; /** Get Image height \return image width */ - FGAPI unsigned height() const; + FGAPI uint height() const; /** Get Image's channel format @@ -80,14 +99,29 @@ class Image { \return OpenGL PBO resource id. */ - FGAPI unsigned pbo() const; + FGAPI uint pbo() const; /** Get the OpenGL Pixel Buffer Object resource size \return OpenGL PBO resource size. */ - FGAPI unsigned size() const; + FGAPI uint size() const; + + /** + Render the image to given window + + \param[in] pWindow is target window to where image will be rendered + \param[in] pX is x coordinate of origin of viewport in window coordinates + \param[in] pY is y coordinate of origin of viewport in window coordinates + \param[in] pVPW is the width of the viewport + \param[in] pVPH is the height of the viewport + \param[in] pTransform is an array of floats. This vector is expected to contain + at least 16 elements + */ + FGAPI void render(const Window& pWindow, + const int pX, const int pY, const int pVPW, const int pVPH, + const std::vector& pTransform) const; /** Get the handle to internal implementation of Image diff --git a/include/fg/plot.h b/include/fg/plot.h index 18b121bb..c4721ac4 100644 --- a/include/fg/plot.h +++ b/include/fg/plot.h @@ -11,6 +11,8 @@ #include +#include + namespace internal { class _Plot; @@ -19,6 +21,8 @@ class _Plot; namespace fg { +class Window; + /** \class Plot @@ -26,7 +30,7 @@ namespace fg */ class Plot { private: - internal::_Plot* value; + internal::_Plot* mValue; public: /** @@ -36,14 +40,15 @@ class Plot { \param[in] pDataType takes one of the values of \ref dtype that indicates the integral data type of plot data */ - FGAPI Plot(unsigned pNumPoints, dtype pDataType, fg::PlotType=fg::FG_LINE, fg::MarkerType=fg::FG_NONE); + FGAPI Plot(const uint pNumPoints, const dtype pDataType, const ChartType pChartType, + const PlotType=FG_LINE, const MarkerType=FG_NONE); /** Copy constructor for Plot \param[in] other is the Plot of which we make a copy of. */ - FGAPI Plot(const Plot& other); + FGAPI Plot(const Plot& pOther); /** Plot Destructor @@ -55,7 +60,7 @@ class Plot { \param[in] col takes values of fg::Color to define plot color */ - FGAPI void setColor(fg::Color col); + FGAPI void setColor(const fg::Color pColor); /** Set the color of line graph(plot) @@ -63,71 +68,62 @@ class Plot { \param[in] pRed is Red component in range [0, 1] \param[in] pGreen is Green component in range [0, 1] \param[in] pBlue is Blue component in range [0, 1] + \param[in] pAlpha is Blue component in range [0, 1] */ - FGAPI void setColor(float pRed, float pGreen, float pBlue); - - /** - Set the chart axes limits - - \param[in] pXmax is X-Axis maximum value - \param[in] pXmin is X-Axis minimum value - \param[in] pYmax is Y-Axis maximum value - \param[in] pYmin is Y-Axis minimum value - */ - FGAPI void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin); + FGAPI void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha); /** - Set axes titles in histogram(bar chart) + Set plot legend - \param[in] pXTitle is X-Axis title - \param[in] pYTitle is Y-Axis title + \param[in] pLegend */ - FGAPI void setAxesTitles(const char* pXTitle, const char* pYTitle); + FGAPI void setLegend(const std::string& pLegend); /** - Get X-Axis maximum value + Get the OpenGL buffer object identifier for vertices - \return Maximum value along X-Axis + \return OpenGL VBO resource id. */ - FGAPI float xmax() const; + FGAPI uint vertices() const; /** - Get X-Axis minimum value + Get the OpenGL buffer object identifier for color values per vertex - \return Minimum value along X-Axis + \return OpenGL VBO resource id. */ - FGAPI float xmin() const; + FGAPI uint colors() const; /** - Get Y-Axis maximum value + Get the OpenGL buffer object identifier for alpha values per vertex - \return Maximum value along Y-Axis + \return OpenGL VBO resource id. */ - FGAPI float ymax() const; + FGAPI uint alphas() const; /** - Get Y-Axis minimum value + Get the OpenGL Vertex Buffer Object resource size - \return Minimum value along Y-Axis + \return vertex buffer object size in bytes */ - FGAPI float ymin() const; + FGAPI uint verticesSize() const; /** - Get the OpenGL Vertex Buffer Object identifier + Get the OpenGL Vertex Buffer Object resource size - \return OpenGL VBO resource id. + \return colors buffer object size in bytes */ - FGAPI unsigned vbo() const; + FGAPI uint colorsSize() const; /** Get the OpenGL Vertex Buffer Object resource size - \return OpenGL VBO resource size. + \return alpha buffer object size in bytes */ - FGAPI unsigned size() const; + FGAPI uint alphasSize() const; /** - Get the handle to internal implementation of Histogram + Get the handle to internal implementation of plot */ FGAPI internal::_Plot* get() const; }; diff --git a/include/fg/plot3.h b/include/fg/plot3.h deleted file mode 100644 index cda08bcf..00000000 --- a/include/fg/plot3.h +++ /dev/null @@ -1,155 +0,0 @@ -/******************************************************* - * Copyright (c) 2015-2019, ArrayFire - * All rights reserved. - * - * This file is distributed under 3-clause BSD license. - * The complete license agreement can be obtained at: - * http://arrayfire.com/licenses/BSD-3-Clause - ********************************************************/ - -#pragma once - -#include - -namespace internal -{ -class _Plot3; -} - -namespace fg -{ - -/** - \class Plot3 - - \brief 3d graph to display 3d line plots. - */ -class Plot3 { - private: - internal::_Plot3* value; - - public: - /** - Creates a Plot3 object - - \param[in] pNumPoints is number of data points - \param[in] pDataType takes one of the values of \ref dtype that indicates - the integral data type of plot data - \param[in] pPlotType is the render type which can be one of \ref PlotType (valid choices - are FG_LINE and FG_SCATTER) - \param[in] pMarkerType is the type of \ref MarkerType to draw for \ref FG_SCATTER plot type - */ - FGAPI Plot3(unsigned pNumPoints, dtype pDataType, PlotType pPlotType=fg::FG_LINE, MarkerType pMarkerType=fg::FG_NONE); - - /** - Copy constructor for Plot3 - - \param[in] other is the Plot3 of which we make a copy of. - */ - FGAPI Plot3(const Plot3& other); - - /** - Plot3 Destructor - */ - FGAPI ~Plot3(); - - /** - Set the color of the 3d line plot - - \param[in] col takes values of fg::Color to define plot color - */ - FGAPI void setColor(fg::Color col); - - /** - Set the color of the 3d line plot - - \param[in] pRed is Red component in range [0, 1] - \param[in] pGreen is Green component in range [0, 1] - \param[in] pBlue is Blue component in range [0, 1] - */ - FGAPI void setColor(float pRed, float pGreen, float pBlue); - - /** - Set the chart axes limits - - \param[in] pXmax is X-Axis maximum value - \param[in] pXmin is X-Axis minimum value - \param[in] pYmax is Y-Axis maximum value - \param[in] pYmin is Y-Axis minimum value - \param[in] pZmax is Z-Axis maximum value - \param[in] pZmin is Z-Axis minimum value - */ - FGAPI void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, float pZmax, float pZmin); - - /** - Set axes titles - - \param[in] pXTitle is X-Axis title - \param[in] pYTitle is Y-Axis title - \param[in] pZTitle is Z-Axis title - */ - FGAPI void setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle); - - /** - Get X-Axis maximum value - - \return Maximum value along X-Axis - */ - FGAPI float xmax() const; - - /** - Get X-Axis minimum value - - \return Minimum value along X-Axis - */ - FGAPI float xmin() const; - - /** - Get Y-Axis maximum value - - \return Maximum value along Y-Axis - */ - FGAPI float ymax() const; - - /** - Get Y-Axis minimum value - - \return Minimum value along Y-Axis - */ - FGAPI float ymin() const; - - /** - Get Z-Axis maximum value - - \return Maximum value along Z-Axis - */ - FGAPI float zmax() const; - - /** - Get Z-Axis minimum value - - \return Minimum value along Z-Axis - */ - FGAPI float zmin() const; - - /** - Get the OpenGL Vertex Buffer Object identifier - - \return OpenGL VBO resource id. - */ - FGAPI unsigned vbo() const; - - /** - Get the OpenGL Vertex Buffer Object resource size - - \return OpenGL VBO resource size. - */ - FGAPI unsigned size() const; - - /** - Get the handle to internal implementation of _Surface - */ - FGAPI internal::_Plot3* get() const; -}; - -} diff --git a/include/fg/surface.h b/include/fg/surface.h index 0348f8d2..3168e4ad 100644 --- a/include/fg/surface.h +++ b/include/fg/surface.h @@ -11,6 +11,8 @@ #include +#include + namespace internal { class _Surface; @@ -19,6 +21,8 @@ class _Surface; namespace fg { +class Window; + /** \class Surface @@ -26,7 +30,7 @@ namespace fg */ class Surface { private: - internal::_Surface* value; + internal::_Surface* mValue; public: /** @@ -40,14 +44,15 @@ class Surface { are FG_SURFACE and FG_SCATTER) \param[in] pMarkerType is the type of \ref MarkerType to draw for \ref FG_SCATTER plot type */ - FGAPI Surface(unsigned pNumXPoints, unsigned pNumYPoints, dtype pDataType, PlotType pPlotType=fg::FG_SURFACE, MarkerType pMarkerType=fg::FG_NONE); + FGAPI Surface(const uint pNumXPoints, const uint pNumYPoints, const dtype pDataType, + const PlotType pPlotType=FG_SURFACE, const MarkerType pMarkerType=FG_NONE); /** Copy constructor for Plot \param[in] other is the Plot of which we make a copy of. */ - FGAPI Surface(const Surface& other); + FGAPI Surface(const Surface& pOther); /** Plot Destructor @@ -59,7 +64,7 @@ class Surface { \param[in] col takes values of fg::Color to define plot color */ - FGAPI void setColor(fg::Color col); + FGAPI void setColor(const fg::Color pColor); /** Set the color of line graph(plot) @@ -67,88 +72,62 @@ class Surface { \param[in] pRed is Red component in range [0, 1] \param[in] pGreen is Green component in range [0, 1] \param[in] pBlue is Blue component in range [0, 1] + \param[in] pAlpha is Blue component in range [0, 1] */ - FGAPI void setColor(float pRed, float pGreen, float pBlue); - - /** - Set the chart axes limits - - \param[in] pXmax is X-Axis maximum value - \param[in] pXmin is X-Axis minimum value - \param[in] pYmax is Y-Axis maximum value - \param[in] pYmin is Y-Axis minimum value - \param[in] pZmax is Z-Axis maximum value - \param[in] pZmin is Z-Axis minimum value - */ - FGAPI void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, float pZmax, float pZmin); + FGAPI void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha); /** - Set axes titles + Set plot legend - \param[in] pXTitle is X-Axis title - \param[in] pYTitle is Y-Axis title - \param[in] pZTitle is Z-Axis title + \param[in] pLegend */ - FGAPI void setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle); + FGAPI void setLegend(const std::string& pLegend); /** - Get X-Axis maximum value + Get the OpenGL buffer object identifier for vertices - \return Maximum value along X-Axis - */ - FGAPI float xmax() const; - - /** - Get X-Axis minimum value - - \return Minimum value along X-Axis - */ - FGAPI float xmin() const; - - /** - Get Y-Axis maximum value - - \return Maximum value along Y-Axis + \return OpenGL VBO resource id. */ - FGAPI float ymax() const; + FGAPI uint vertices() const; /** - Get Y-Axis minimum value + Get the OpenGL buffer object identifier for color values per vertex - \return Minimum value along Y-Axis + \return OpenGL VBO resource id. */ - FGAPI float ymin() const; + FGAPI uint colors() const; /** - Get Z-Axis maximum value + Get the OpenGL buffer object identifier for alpha values per vertex - \return Maximum value along Z-Axis + \return OpenGL VBO resource id. */ - FGAPI float zmax() const; + FGAPI uint alphas() const; /** - Get Z-Axis minimum value + Get the OpenGL Vertex Buffer Object resource size - \return Minimum value along Z-Axis + \return vertex buffer object size in bytes */ - FGAPI float zmin() const; + FGAPI uint verticesSize() const; /** - Get the OpenGL Vertex Buffer Object identifier + Get the OpenGL Vertex Buffer Object resource size - \return OpenGL VBO resource id. + \return colors buffer object size in bytes */ - FGAPI unsigned vbo() const; + FGAPI uint colorsSize() const; /** Get the OpenGL Vertex Buffer Object resource size - \return OpenGL VBO resource size. + \return alpha buffer object size in bytes */ - FGAPI unsigned size() const; + FGAPI uint alphasSize() const; /** - Get the handle to internal implementation of _Surface + Get the handle to internal implementation of surface */ FGAPI internal::_Surface* get() const; }; diff --git a/include/fg/window.h b/include/fg/window.h index 358b6497..9ad72813 100644 --- a/include/fg/window.h +++ b/include/fg/window.h @@ -12,8 +12,7 @@ #include #include #include -#include -#include +#include #include #include @@ -32,7 +31,7 @@ namespace fg */ class Window { private: - internal::_Window* value; + internal::_Window* mValue; Window() {} @@ -166,47 +165,15 @@ class Window { FGAPI void draw(const Image& pImage, const bool pKeepAspectRatio=true); /** - Render a Plot to Window + Render a chart to Window - \param[in] pPlot is an object of class Plot + \param[in] pChart is an chart object \note this draw call does a OpenGL swap buffer, so we do not need to call Window::draw() after this function is called upon for rendering a plot */ - FGAPI void draw(const Plot& pPlot); - - /** - Render a Plot3 to Window - - \param[in] pPlot3 is an object of class Plot3 - - \note this draw call does a OpenGL swap buffer, so we do not need - to call Window::draw() after this function is called upon for rendering - a plot - */ - FGAPI void draw(const Plot3& pPlot3); - - /** - Render a Surface to Window - - \param[in] pSurface is an object of class Surface - - \note this draw call does a OpenGL swap buffer, so we do not need - to call Window::draw() after this function is called upon for rendering - a plot - */ - FGAPI void draw(const Surface& pSurface); - /** - Render Histogram to Window - - \param[in] pHist is an object of class Histogram - - \note this draw call does a OpenGL swap buffer, so we do not need - to call Window::draw() after this function is called upon for rendering - a histogram - */ - FGAPI void draw(const Histogram& pHist); + FGAPI void draw(const Chart& pChart); /** Setup grid layout for multivew mode @@ -241,72 +208,14 @@ class Window { FGAPI void draw(int pColId, int pRowId, const Image& pImage, const char* pTitle=0, const bool pKeepAspectRatio=true); /** - Render Plot to given sub-region of the window in multiview mode - - Window::grid should have been already called before any of the draw calls - that accept coloum index and row index is used to render an object. - - \param[in] pColId is coloumn index - \param[in] pRowId is row index - \param[in] pPlot is an object of class Plot - \param[in] pTitle is the title that will be displayed for the cell represented - by \p pColId and \p pRowId - - \note This draw call doesn't do OpenGL swap buffer since it doesn't have the - knowledge of which sub-regions already got rendered. We should call - Window::draw() once all draw calls corresponding to all sub-regions are called - when in multiview mode. - */ - FGAPI void draw(int pColId, int pRowId, const Plot& pPlot, const char* pTitle = 0); - - - /** - Render Plot3 to given sub-region of the window in multiview mode - - Window::grid should have been already called before any of the draw calls - that accept coloum index and row index is used to render an object. - - \param[in] pColId is coloumn index - \param[in] pRowId is row index - \param[in] pPlot3 is an object of class Plot3 - \param[in] pTitle is the title that will be displayed for the cell represented - by \p pColId and \p pRowId - - \note This draw call doesn't do OpenGL swap buffer since it doesn't have the - knowledge of which sub-regions already got rendered. We should call - Window::draw() once all draw calls corresponding to all sub-regions are called - when in multiview mode. - */ - FGAPI void draw(int pColId, int pRowId, const Plot3& pPlot3, const char* pTitle = 0); - - /** - Render Surface to given sub-region of the window in multiview mode - - Window::grid should have been already called before any of the draw calls - that accept coloum index and row index is used to render an object. - - \param[in] pColId is coloumn index - \param[in] pRowId is row index - \param[in] pSurface is an object of class Surface - \param[in] pTitle is the title that will be displayed for the cell represented - by \p pColId and \p pRowId - - \note This draw call doesn't do OpenGL swap buffer since it doesn't have the - knowledge of which sub-regions already got rendered. We should call - Window::draw() once all draw calls corresponding to all sub-regions are called - when in multiview mode. - */ - FGAPI void draw(int pColId, int pRowId, const Surface& pSurface, const char* pTitle = 0); - - /** - Render Histogram to given sub-region of the window in multiview mode + Render the chart to given sub-region of the window in multiview mode Window::grid should have been already called before any of the draw calls that accept coloum index and row index is used to render an object. \param[in] pColId is coloumn index \param[in] pRowId is row index - \param[in] pHist is an object of class Histogram + \param[in] pChart is a Chart with one or more plottable renderables \param[in] pTitle is the title that will be displayed for the cell represented by \p pColId and \p pRowId @@ -315,7 +224,7 @@ class Window { Window::draw() once all draw calls corresponding to all sub-regions are called when in multiview mode. */ - FGAPI void draw(int pColId, int pRowId, const Histogram& pHist, const char* pTitle = 0); + FGAPI void draw(int pColId, int pRowId, const Chart& pChart, const char* pTitle = 0); /** Swaps background OpenGL buffer with front buffer diff --git a/include/forge.h b/include/forge.h index c17c8ec1..dd8e0edc 100644 --- a/include/forge.h +++ b/include/forge.h @@ -16,6 +16,5 @@ #include "fg/image.h" #include "fg/version.h" #include "fg/plot.h" -#include "fg/plot3.h" #include "fg/surface.h" #include "fg/histogram.h" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8546b58..6fd340ba 100755 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,8 @@ CMAKE_MINIMUM_REQUIRED(VERSION 2.8) FIND_PACKAGE(GLM QUIET) FIND_PACKAGE(glm QUIET) +INCLUDE("${CMAKE_MODULE_PATH}/GLSLtoH.cmake") + IF((NOT glm_FOUND AND NOT GLM_FOUND) OR (${USE_LOCAL_GLM})) SET(USE_LOCAL_GLM ON) MESSAGE(STATUS "Downloading GLM headers.") @@ -67,6 +69,7 @@ INCLUDE_DIRECTORIES( ${GLM_INCLUDE_DIRS} ${WTK_INCLUDE_DIRS} "${PROJECT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_BINARY_DIR}" ) IF(UNIX) @@ -110,6 +113,22 @@ ELSEIF(${USE_WINDOW_TOOLKIT} STREQUAL "sdl2") ENDIF() +SET(glsl_shader_headers + "shader_headers") +FILE(GLOB glsl_shaders + "shaders/*.glsl") +SOURCE_GROUP(Shaders FILES ${glsl_shaders}) + +GLSL_TO_H( + SOURCES ${glsl_shaders} + VARNAME shader_files + EXTENSION "hpp" + OUTPUT_DIR ${glsl_shader_headers} + TARGETS glsl_shader_targets + NAMESPACE "glsl" + EOD "0" + ) + ADD_LIBRARY(forge SHARED ${api_headers} ${headers} @@ -127,6 +146,8 @@ TARGET_LINK_LIBRARIES(forge PRIVATE ${X11_LIBS} ) +ADD_DEPENDENCIES(forge ${glsl_shader_targets}) + INSTALL(TARGETS forge EXPORT FORGE DESTINATION "${FG_INSTALL_LIB_DIR}" diff --git a/src/chart.cpp b/src/chart.cpp index 9bb05dfb..828cece4 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -7,52 +7,34 @@ * http://arrayfire.com/licenses/BSD-3-Clause ********************************************************/ -#include +#include +#include +#include +#include #include - -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include +#include +#include + using namespace std; typedef std::vector::const_iterator StringIter; static const int CHART2D_FONT_SIZE = 15; -const char *gChartVertexShaderSrc = -"#version 330\n" -"in vec3 point;\n" -"uniform mat4 transform;\n" -"void main(void) {\n" -" gl_Position = transform * vec4(point.xyz, 1);\n" -"}"; - -const char *gChartFragmentShaderSrc = -"#version 330\n" -"uniform vec4 color;\n" -"out vec4 outputColor;\n" -"void main(void) {\n" -" outputColor = color;\n" -"}"; - -const char *gChartSpriteFragmentShaderSrc = -"#version 330\n" -"uniform bool isYAxis;\n" -"uniform vec4 tick_color;\n" -"out vec4 outputColor;\n" -"void main(void) {\n" -" bool y_axis = isYAxis && abs(gl_PointCoord.y)>0.2;\n" -" bool x_axis = !isYAxis && abs(gl_PointCoord.x)>0.2;\n" -" if(y_axis || x_axis)\n" -" discard;\n" -" else\n" -" outputColor = tick_color;\n" -"}"; - const std::shared_ptr& getChartFont() { static internal::_Font mChartFont; @@ -89,33 +71,34 @@ namespace internal /********************* BEGIN-AbstractChart *********************/ -void AbstractChart::renderTickLabels(int pWindowId, unsigned w, unsigned h, - std::vector &texts, - glm::mat4 &transformation, int coor_offset, - bool useZoffset) +void AbstractChart::renderTickLabels( + const int pWindowId, const uint pW, const uint pH, + const std::vector &pTexts, + const glm::mat4 &pTransformation, const int pCoordsOffset, + const bool pUseZoffset) const { auto &fonter = getChartFont(); - fonter->setOthro2D(int(w), int(h)); + fonter->setOthro2D(int(pW), int(pH)); float pos[2]; - for (StringIter it = texts.begin(); it!=texts.end(); ++it) { - int idx = int(it - texts.begin()); - glm::vec4 p = glm::vec4(mTickTextX[idx+coor_offset], - mTickTextY[idx+coor_offset], - (useZoffset ? mTickTextZ[idx+coor_offset] : 0), 1); - glm::vec4 res = transformation * p; + for (StringIter it = pTexts.begin(); it!=pTexts.end(); ++it) { + int idx = int(it - pTexts.begin()); + glm::vec4 p = glm::vec4(mTickTextX[idx+pCoordsOffset], + mTickTextY[idx+pCoordsOffset], + (pUseZoffset ? mTickTextZ[idx+pCoordsOffset] : 0), 1); + glm::vec4 res = pTransformation * p; /* convert text position from [-1,1] range to * [0, 1) range and then offset horizontally * to compensate for margins and ticksize */ - pos[0] = w*(res.x/res.w+1.0f)/2.0f; - pos[1] = h*(res.y/res.w+1.0f)/2.0f; + pos[0] = pW * (res.x/res.w+1.0f)/2.0f; + pos[1] = pH * (res.y/res.w+1.0f)/2.0f; /* offset based on text size to align * text center with tick mark position */ - if(coor_offset < mTickCount) { + if(pCoordsOffset < mTickCount) { pos[0] -= ((CHART2D_FONT_SIZE*it->length()/2.0f)); - }else if(coor_offset >= mTickCount && coor_offset < 2*mTickCount) { + }else if(pCoordsOffset >= mTickCount && pCoordsOffset < 2*mTickCount) { pos[1] -= ((CHART2D_FONT_SIZE)); }else { pos[0] -= ((CHART2D_FONT_SIZE*it->length()/2.0f)); @@ -125,7 +108,8 @@ void AbstractChart::renderTickLabels(int pWindowId, unsigned w, unsigned h, } } -AbstractChart::AbstractChart(int pLeftMargin, int pRightMargin, int pTopMargin, int pBottomMargin) +AbstractChart::AbstractChart(const int pLeftMargin, const int pRightMargin, + const int pTopMargin, const int pBottomMargin) : mTickCount(9), mTickSize(10), mLeftMargin(pLeftMargin), mRightMargin(pRightMargin), mTopMargin(pTopMargin), mBottomMargin(pBottomMargin), @@ -144,8 +128,8 @@ AbstractChart::AbstractChart(int pLeftMargin, int pRightMargin, int pTopMargin, * are loaded into the shared Font object */ getChartFont(); - mBorderProgram = initShaders(gChartVertexShaderSrc, gChartFragmentShaderSrc); - mSpriteProgram = initShaders(gChartVertexShaderSrc, gChartSpriteFragmentShaderSrc); + mBorderProgram = initShaders(glsl::chart_vs.c_str(), glsl::chart_fs.c_str()); + mSpriteProgram = initShaders(glsl::chart_vs.c_str(), glsl::tick_fs.c_str()); mBorderAttribPointIndex = glGetAttribLocation (mBorderProgram, "point"); mBorderUniformColorIndex = glGetUniformLocation(mBorderProgram, "color"); @@ -171,9 +155,9 @@ AbstractChart::~AbstractChart() CheckGL("End AbstractChart::~AbstractChart"); } -void AbstractChart::setAxesLimits(float pXmax, float pXmin, - float pYmax, float pYmin, - float pZmax, float pZmin) +void AbstractChart::setAxesLimits(const float pXmin, const float pXmax, + const float pYmin, const float pYmax, + const float pZmin, const float pZmax) { mXMax = pXmax; mXMin = pXmin; mYMax = pYmax; mYMin = pYmin; @@ -188,7 +172,9 @@ void AbstractChart::setAxesLimits(float pXmax, float pXmin, generateTickLabels(); } -void AbstractChart::setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle) +void AbstractChart::setAxesTitles(const std::string& pXTitle, + const std::string& pYTitle, + const std::string& pZTitle) { mXTitle = std::string(pXTitle); mYTitle = std::string(pYTitle); @@ -202,15 +188,20 @@ float AbstractChart::ymin() const { return mYMin; } float AbstractChart::zmax() const { return mZMax; } float AbstractChart::zmin() const { return mZMin; } +void AbstractChart::addRenderable(const std::shared_ptr pRenderable) +{ + mRenderables.emplace_back(pRenderable); +} + /********************* END-AbstractChart *********************/ -/********************* BEGIN-Chart2D *********************/ +/********************* BEGIN-chart2d_impl *********************/ -void Chart2D::bindResources(int pWindowId) +void chart2d_impl::bindResources(const int pWindowId) { - CheckGL("Begin Chart2D::bindResources"); + CheckGL("Begin chart2d_impl::bindResources"); if (mVAOMap.find(pWindowId) == mVAOMap.end()) { GLuint vao = 0; /* create a vertex array object @@ -226,23 +217,23 @@ void Chart2D::bindResources(int pWindowId) mVAOMap[pWindowId] = vao; } glBindVertexArray(mVAOMap[pWindowId]); - CheckGL("End Chart2D::bindResources"); + CheckGL("End chart2d_impl::bindResources"); } -void Chart2D::unbindResources() const +void chart2d_impl::unbindResources() const { glBindVertexArray(0); } -void Chart2D::pushTicktextCoords(float x, float y, float z) +void chart2d_impl::pushTicktextCoords(const float pX, const float pY, const float pZ) { - mTickTextX.push_back(x); - mTickTextY.push_back(y); + mTickTextX.push_back(pX); + mTickTextY.push_back(pY); } -void Chart2D::generateChartData() +void chart2d_impl::generateChartData() { - CheckGL("Begin Chart2D::generateChartData"); + CheckGL("Begin chart2d_impl::generateChartData"); static const float border[8] = { -1, -1, 1, -1, 1, 1, -1, 1 }; static const int nValues = sizeof(border)/sizeof(float); @@ -307,10 +298,10 @@ void Chart2D::generateChartData() /* create vbo that has the border and axis data */ mDecorVBO = createBuffer(GL_ARRAY_BUFFER, decorData.size(), &(decorData.front()), GL_STATIC_DRAW); - CheckGL("End Chart2D::generateChartData"); + CheckGL("End chart2d_impl::generateChartData"); } -void Chart2D::generateTickLabels() +void chart2d_impl::generateTickLabels() { /* remove all the tick text markers that were generated * by default during the base class(chart) creation and @@ -339,9 +330,16 @@ void Chart2D::generateTickLabels() } } -void Chart2D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) +chart2d_impl::chart2d_impl() + :AbstractChart(68, 8, 8, 32) { + generateChartData(); +} + +void chart2d_impl::render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) { - CheckGL("Begin Chart2D::renderChart"); + CheckGL("Begin chart2d_impl::renderChart"); float w = float(pVPW - (mLeftMargin + mRightMargin + mTickSize)); float h = float(pVPH - (mTopMargin + mBottomMargin + mTickSize)); @@ -350,7 +348,7 @@ void Chart2D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) float scale_x = w / pVPW; float scale_y = h / pVPH; - Chart2D::bindResources(pWindowId); + chart2d_impl::bindResources(pWindowId); /* bind the plotting shader program */ glUseProgram(mBorderProgram); @@ -385,7 +383,7 @@ void Chart2D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) glUseProgram(0); glPointSize(1); - Chart2D::unbindResources(); + chart2d_impl::unbindResources(); renderTickLabels(pWindowId, int(w), int(h), mYText, trans, 0, false); renderTickLabels(pWindowId, int(w), int(h), mXText, trans, mTickCount, false); @@ -408,19 +406,26 @@ void Chart2D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) pos[1] += (mTickSize * (h/pVPH)); fonter->render(pWindowId, pos, WHITE, mXTitle.c_str(), CHART2D_FONT_SIZE); } + /* render all the renderables */ + // FIXME create the correct transformation matrix + glm::mat4 transMat = glm::mat4(1); + for (auto renderable : mRenderables) { + renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, transMat); + } - CheckGL("End Chart2D::renderChart"); + CheckGL("End chart2d_impl::renderChart"); } -/********************* END-Chart2D *********************/ +/********************* END-chart2d_impl *********************/ -/********************* BEGIN-Chart3D *********************/ +/********************* BEGIN-chart3d_impl *********************/ -void Chart3D::bindResources(int pWindowId) +void chart3d_impl::bindResources(const int pWindowId) { - CheckGL("Begin Chart3D::bindResources"); + CheckGL("Begin chart3d_impl::bindResources"); if (mVAOMap.find(pWindowId) == mVAOMap.end()) { GLuint vao = 0; /* create a vertex array object @@ -436,24 +441,24 @@ void Chart3D::bindResources(int pWindowId) mVAOMap[pWindowId] = vao; } glBindVertexArray(mVAOMap[pWindowId]); - CheckGL("End Chart3D::bindResources"); + CheckGL("End chart3d_impl::bindResources"); } -void Chart3D::unbindResources() const +void chart3d_impl::unbindResources() const { glBindVertexArray(0); } -void Chart3D::pushTicktextCoords(float x, float y, float z) +void chart3d_impl::pushTicktextCoords(const float pX, const float pY, const float pZ) { - mTickTextX.push_back(x); - mTickTextY.push_back(y); - mTickTextZ.push_back(z); + mTickTextX.push_back(pX); + mTickTextY.push_back(pY); + mTickTextZ.push_back(pZ); } -void Chart3D::generateChartData() +void chart3d_impl::generateChartData() { - CheckGL("Begin Chart3D::generateChartData"); + CheckGL("Begin chart3d_impl::generateChartData"); static const float border[] = { -1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, 1, 1, -1 }; static const int nValues = sizeof(border)/sizeof(float); @@ -540,10 +545,10 @@ void Chart3D::generateChartData() /* create vbo that has the border and axis data */ mDecorVBO = createBuffer(GL_ARRAY_BUFFER, decorData.size(), &(decorData.front()), GL_STATIC_DRAW); - CheckGL("End Chart3D::generateChartData"); + CheckGL("End chart3d_impl::generateChartData"); } -void Chart3D::generateTickLabels() +void chart3d_impl::generateTickLabels() { /* remove all the tick text markers that were generated * by default during the base class(chart) creation and @@ -579,13 +584,20 @@ void Chart3D::generateTickLabels() } } -void Chart3D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) +chart3d_impl::chart3d_impl() + :AbstractChart(32, 32, 32, 32) { + generateChartData(); +} + +void chart3d_impl::render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) { - CheckGL("Being Chart3D::renderChart"); + CheckGL("Being chart3d_impl::renderChart"); float w = float(pVPW - (mLeftMargin + mRightMargin + mTickSize)); float h = float(pVPH - (mTopMargin + mBottomMargin + mTickSize)); - Chart3D::bindResources(pWindowId); + chart3d_impl::bindResources(pWindowId); /* bind the plotting shader program */ glUseProgram(mBorderProgram); @@ -628,7 +640,7 @@ void Chart3D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) glUseProgram(0); glPointSize(1); glDisable(GL_PROGRAM_POINT_SIZE); - Chart3D::unbindResources(); + chart3d_impl::unbindResources(); renderTickLabels(pWindowId, w, h, mZText, trans, 0); renderTickLabels(pWindowId, w, h, mYText, trans, mTickCount); @@ -663,7 +675,130 @@ void Chart3D::renderChart(int pWindowId, int pX, int pY, int pVPW, int pVPH) fonter->render(pWindowId, pos, WHITE, mXTitle.c_str(), CHART2D_FONT_SIZE); } - CheckGL("End Chart3D::renderChart"); + /* render all the renderables */ + // FIXME create the correct transformation matrix + glm::mat4 transMat = glm::mat4(1); + for (auto renderable : mRenderables) { + renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, transMat); + } + + CheckGL("End chart3d_impl::renderChart"); +} + +} + +namespace fg +{ + +Chart::Chart(const ChartType cType) + : mChartType(cType) +{ + mValue = new internal::_Chart(cType); +} + +Chart::~Chart() +{ + delete mValue; +} + +void Chart::setAxesTitles(const std::string pX, + const std::string pY, + const std::string pZ) +{ + mValue->setAxesTitles(pX, pY, pZ); +} + +void Chart::setAxesLimits(const float pXmin, const float pXmax, + const float pYmin, const float pYmax, + const float pZmin, const float pZmax) +{ + mValue->setAxesLimits(pXmin, pXmax, pYmin, pYmax, pZmin, pZmax); +} + +void Chart::add(const Image& pImage) +{ + mValue->addRenderable(pImage.get()->impl()); +} + +void Chart::add(const Histogram& pHistogram) +{ + mValue->addRenderable(pHistogram.get()->impl()); +} + +void Chart::add(const Plot& pPlot) +{ + mValue->addRenderable(pPlot.get()->impl()); +} + +void Chart::add(const Surface& pSurface) +{ + mValue->addRenderable(pSurface.get()->impl()); +} + +Image Chart::image(const uint pWidth, const uint pHeight, + const ChannelFormat pFormat, const dtype pDataType) +{ + Image retVal(pWidth, pHeight, pFormat, pDataType); + mValue->addRenderable(retVal.get()->impl()); + return retVal; +} + +Histogram Chart::histogram(const uint pNBins, const dtype pDataType) +{ + if (mChartType == FG_2D) { + Histogram retVal(pNBins, pDataType); + mValue->addRenderable(retVal.get()->impl()); + return retVal; + } else { + throw ArgumentError("Chart::render", __LINE__, 5, + "Can add histogram to a 2d chart only"); + } +} + +Plot Chart::plot(const uint pNumPoints, const dtype pDataType, + const PlotType pPlotType, const MarkerType pMarkerType) +{ + if (mChartType == FG_2D) { + Plot retVal(pNumPoints, pDataType, FG_2D, pPlotType, pMarkerType); + mValue->addRenderable(retVal.get()->impl()); + return retVal; + } else { + Plot retVal(pNumPoints, pDataType, FG_3D, pPlotType, pMarkerType); + mValue->addRenderable(retVal.get()->impl()); + return retVal; + } +} + +Surface Chart::surface(const uint pNumXPoints, const uint pNumYPoints, const dtype pDataType, + const PlotType pPlotType, const MarkerType pMarkerType) +{ + if (mChartType == FG_3D) { + Surface retVal(pNumXPoints, pNumYPoints, pDataType, pPlotType, pMarkerType); + mValue->addRenderable(retVal.get()->impl()); + return retVal; + } else { + throw ArgumentError("Chart::render", __LINE__, 5, + "Can add surface plot to a 3d chart only"); + } +} + +void Chart::render(const Window& pWindow, + const int pX, const int pY, const int pVPW, const int pVPH, + const std::vector& pTransform) const +{ + if (pTransform.size() < 16) { + throw ArgumentError("Chart::render", __LINE__, 5, + "Insufficient transform matrix data"); + } + mValue->render(pWindow.get()->getID(), + pX, pY, pVPW, pVPH, + glm::make_mat4(pTransform.data())); +} + +internal::_Chart* Chart::get() const +{ + return mValue; } } diff --git a/src/chart.hpp b/src/chart.hpp index f29e45f5..c1459ac3 100644 --- a/src/chart.hpp +++ b/src/chart.hpp @@ -10,11 +10,12 @@ #pragma once #include +#include + +#include +#include #include #include -#include - -#include namespace internal { @@ -29,58 +30,65 @@ class AbstractChart : public AbstractRenderable { std::vector mXText; std::vector mYText; std::vector mZText; - int mTickCount; /* should be an odd number always */ - int mTickSize; - int mLeftMargin; - int mRightMargin; - int mTopMargin; - int mBottomMargin; + int mTickCount; /* should be an odd number always */ + int mTickSize; + int mLeftMargin; + int mRightMargin; + int mTopMargin; + int mBottomMargin; /* chart axes ranges and titles */ - float mXMax; - float mXMin; - float mYMax; - float mYMin; - float mZMax; - float mZMin; + float mXMax; + float mXMin; + float mYMax; + float mYMin; + float mZMax; + float mZMin; std::string mXTitle; std::string mYTitle; std::string mZTitle; /* OpenGL Objects */ - GLuint mDecorVBO; - GLuint mBorderProgram; - GLuint mSpriteProgram; + GLuint mDecorVBO; + GLuint mBorderProgram; + GLuint mSpriteProgram; /* shader uniform variable locations */ - GLint mBorderAttribPointIndex; - GLint mBorderUniformColorIndex; - GLint mBorderUniformMatIndex; - GLint mSpriteUniformMatIndex; - GLint mSpriteUniformTickcolorIndex; - GLint mSpriteUniformTickaxisIndex; + GLuint mBorderAttribPointIndex; + GLuint mBorderUniformColorIndex; + GLuint mBorderUniformMatIndex; + GLuint mSpriteUniformMatIndex; + GLuint mSpriteUniformTickcolorIndex; + GLuint mSpriteUniformTickaxisIndex; /* VAO map to store a vertex array object * for each valid window context */ std::map mVAOMap; + /* list of renderables to be displayed on the chart*/ + std::vector< std::shared_ptr > mRenderables; /* rendering helper functions */ - void renderTickLabels(int pWindowId, unsigned w, unsigned h, - std::vector &texts, - glm::mat4 &transformation, int coor_offset, - bool useZoffset=true); + void renderTickLabels(const int pWindowId, const uint pW, const uint pH, + const std::vector &pTexts, + const glm::mat4 &pTransformation, const int pCoordsOffset, + const bool pUseZoffset=true) const; /* virtual functions that has to be implemented by - * dervied class: Chart2D, Chart3D */ - virtual void bindResources(int pWindowId) = 0; + * dervied class: chart2d_impl, chart3d_impl */ + virtual void bindResources(const int pWindowId) = 0; virtual void unbindResources() const = 0; - virtual void pushTicktextCoords(float x, float y, float z=0.0) = 0; + virtual void pushTicktextCoords(const float pX, const float pY, const float pZ=0.0) = 0; virtual void generateChartData() = 0; virtual void generateTickLabels() = 0; public: - AbstractChart(int pLeftMargin, int pRightMargin, int pTopMargin, int pBottomMargin); + AbstractChart(const int pLeftMargin, const int pRightMargin, + const int pTopMargin, const int pBottomMargin); virtual ~AbstractChart(); - void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, - float pZmax=1, float pZmin=-1); - void setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle="Z-Axis"); + void setAxesTitles(const std::string& pXTitle, + const std::string& pYTitle, + const std::string& pZTitle); + + void setAxesLimits(const float pXmin, const float pXmax, + const float pYmin, const float pYmax, + const float pZmin, const float pZmax); float xmax() const; float xmin() const; @@ -89,71 +97,93 @@ class AbstractChart : public AbstractRenderable { float zmax() const; float zmin() const; - virtual GLuint vbo() const = 0; - virtual size_t size() const = 0; - virtual void renderChart(int pWindowId, int pX, int pY, - int pViewPortWidth, int pViewPortHeight) = 0; - /* Below is pure virtual function of AbstractRenderable */ - virtual void render(int pWindowId, int pX, int pY, - int pViewPortWidth, int pViewPortHeight) = 0; + void addRenderable(const std::shared_ptr pRenderable); }; -class Chart2D : public AbstractChart { +class chart2d_impl : public AbstractChart { private: /* rendering helper functions that are derived * from AbstractRenderable base class * */ - void bindResources(int pWindowId); + void bindResources(const int pWindowId); void unbindResources() const; - void pushTicktextCoords(float x, float y, float z=0.0); + void pushTicktextCoords(const float x, const float y, const float z=0.0); void generateChartData(); void generateTickLabels(); public: - Chart2D() - :AbstractChart(68, 8, 8, 32) { - generateChartData(); - } - virtual ~Chart2D() {} + chart2d_impl(); - void renderChart(int pWindowId, int pX, int pY, - int pViewPortWidth, int pViewPortHeight); + virtual ~chart2d_impl() {} - /* Below pure virtual functions have to - * be implemented by Concrete classes - * which have Chart2D as base class - * */ - virtual GLuint vbo() const = 0; - virtual size_t size() const = 0; - virtual void render(int pWindowId, int pX, int pY, - int pViewPortWidth, int pViewPortHeight) = 0; + void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform); }; -class Chart3D : public AbstractChart { +class chart3d_impl : public AbstractChart { private: /* rendering helper functions that are derived * from AbstractRenderable base class * */ - void bindResources(int pWindowId); + void bindResources(const int pWindowId); void unbindResources() const; - void pushTicktextCoords(float x, float y, float z=0.0); + void pushTicktextCoords(const float x, const float y, const float z=0.0); void generateChartData(); void generateTickLabels(); public: - Chart3D() - :AbstractChart(32, 32, 32, 32) { - generateChartData(); + chart3d_impl(); + + virtual ~chart3d_impl() {} + + void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform); +}; + +class _Chart { + private: + std::shared_ptr mChart; + + public: + _Chart(const fg::ChartType cType) { + if (cType == fg::FG_2D) { + mChart = std::make_shared(); + } else if (cType == fg::FG_3D) { + mChart = std::make_shared(); + } else { + throw fg::ArgumentError("_Chart::_Chart", + __LINE__, 0, + "Invalid chart type"); + } + } + + inline const std::shared_ptr& impl() const { + return mChart; } - virtual ~Chart3D() {} - void renderChart(int pWindowId, int pX, int pY, - int pViewPortWidth, int pViewPortHeight); + inline void setAxesTitles(const std::string& pX, + const std::string& pY, + const std::string& pZ) { + mChart->setAxesTitles(pX, pY, pZ); + } + + inline void setAxesLimits(const float pXmin, const float pXmax, + const float pYmin, const float pYmax, + const float pZmin, const float pZmax) { + mChart->setAxesLimits(pXmin, pXmax, pYmin, pYmax, pZmin, pZmax); + } - virtual GLuint vbo() const = 0; - virtual size_t size() const = 0; - virtual void render(int pWindowId, int pX, int pY, - int pViewPortWidth, int pViewPortHeight) = 0; + inline void addRenderable(const std::shared_ptr pRenderable) { + mChart->addRenderable(pRenderable); + } + + inline void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4 &pTransform) const { + mChart->render(pWindowId, pX, pY, pVPW, pVPH, pTransform); + } }; } diff --git a/src/common.cpp b/src/common.cpp index e54bf8cf..8da2b57e 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -22,11 +22,11 @@ using namespace std; typedef struct { GLuint vertex; GLuint fragment; -} shaders_t; +} Shaders; -GLenum gl_dtype(fg::dtype val) +GLenum dtype2gl(const fg::dtype pValue) { - switch(val) { + switch(pValue) { case s8: return GL_BYTE; case u8: return GL_UNSIGNED_BYTE; case s32: return GL_INT; @@ -37,9 +37,9 @@ GLenum gl_dtype(fg::dtype val) } } -GLenum gl_ctype(ChannelFormat mode) +GLenum ctype2gl(const ChannelFormat pMode) { - switch(mode) { + switch(pMode) { case FG_GRAYSCALE: return GL_RED; case FG_RG : return GL_RG; case FG_RGB : return GL_RGB; @@ -49,93 +49,72 @@ GLenum gl_ctype(ChannelFormat mode) } } -GLenum gl_ictype(ChannelFormat mode) +GLenum ictype2gl(const ChannelFormat pMode) { - if (mode==FG_GRAYSCALE) + if (pMode==FG_GRAYSCALE) return GL_RED; - else if (mode==FG_RG) + else if (pMode==FG_RG) return GL_RG; - else if (mode==FG_RGB || mode==FG_BGR) + else if (pMode==FG_RGB || pMode==FG_BGR) return GL_RGB; - else - return GL_RGBA; -} - -char* loadFile(const char * fname, GLint &fSize) -{ - std::ifstream file(fname,std::ios::in|std::ios::binary|std::ios::ate); - if (file.is_open()) - { - unsigned int size = (unsigned int)file.tellg(); - fSize = size; - char *memblock = new char [size]; - file.seekg (0, std::ios::beg); - file.read (memblock, size); - file.close(); - std::cerr << "file " << fname << " loaded" << std::endl; - return memblock; - } - char buffer[64]; - sprintf(buffer, "Unable to open file %s", fname); - - throw fg::Error("loadFile", __LINE__, buffer, FG_ERR_GL_ERROR); + return GL_RGBA; } -void printShaderInfoLog(GLint shader) +void printShaderInfoLog(GLint pShader) { int infoLogLen = 0; int charsWritten = 0; GLchar *infoLog; - glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLen); + glGetShaderiv(pShader, GL_INFO_LOG_LENGTH, &infoLogLen); - if (infoLogLen > 1) - { + if (infoLogLen > 1) { infoLog = new GLchar[infoLogLen]; - glGetShaderInfoLog(shader,infoLogLen, &charsWritten, infoLog); + glGetShaderInfoLog(pShader, infoLogLen, &charsWritten, infoLog); std::cerr << "InfoLog:" << std::endl << infoLog << std::endl; delete [] infoLog; - throw fg::Error("printShaderInfoLog", __LINE__, "OpenGL Shader compilation failed", FG_ERR_GL_ERROR); + throw fg::Error("printShaderInfoLog", __LINE__, + "OpenGL Shader compilation failed", FG_ERR_GL_ERROR); } } -void printLinkInfoLog(GLint prog) +void printLinkInfoLog(GLint pProgram) { int infoLogLen = 0; int charsWritten = 0; GLchar *infoLog; - glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &infoLogLen); + glGetProgramiv(pProgram, GL_INFO_LOG_LENGTH, &infoLogLen); - if (infoLogLen > 1) - { + if (infoLogLen > 1) { infoLog = new GLchar[infoLogLen]; // error check for fail to allocate memory omitted - glGetProgramInfoLog(prog,infoLogLen, &charsWritten, infoLog); + glGetProgramInfoLog(pProgram, infoLogLen, &charsWritten, infoLog); std::cerr << "InfoLog:" << std::endl << infoLog << std::endl; delete [] infoLog; - throw fg::Error("printLinkInfoLog", __LINE__, "OpenGL Shader linking failed", FG_ERR_GL_ERROR); + throw fg::Error("printLinkInfoLog", __LINE__, + "OpenGL Shader linking failed", FG_ERR_GL_ERROR); } } -void attachAndLinkProgram(GLuint program, shaders_t shaders) +void attachAndLinkProgram(GLuint pProgram, Shaders pShaders) { - glAttachShader(program, shaders.vertex); - glAttachShader(program, shaders.fragment); + glAttachShader(pProgram, pShaders.vertex); + glAttachShader(pProgram, pShaders.fragment); - glLinkProgram(program); + glLinkProgram(pProgram); GLint linked; - glGetProgramiv(program,GL_LINK_STATUS, &linked); - if (!linked) - { + glGetProgramiv(pProgram,GL_LINK_STATUS, &linked); + if (!linked) { std::cerr << "Program did not link." << std::endl; - throw fg::Error("attachAndLinkProgram", __LINE__, "OpenGL program linking failed", FG_ERR_GL_ERROR); + throw fg::Error("attachAndLinkProgram", __LINE__, + "OpenGL program linking failed", FG_ERR_GL_ERROR); } - printLinkInfoLog(program); + printLinkInfoLog(pProgram); } -shaders_t loadShaders(const char * vert_code, const char * frag_code) +Shaders loadShaders(const char* pVertexShaderSrc, const char* pFragmentShaderSrc) { GLuint f, v; @@ -143,55 +122,55 @@ shaders_t loadShaders(const char * vert_code, const char * frag_code) f = glCreateShader(GL_FRAGMENT_SHADER); // load shaders & get length of each - glShaderSource(v, 1, &vert_code, NULL); - glShaderSource(f, 1, &frag_code, NULL); + glShaderSource(v, 1, &pVertexShaderSrc, NULL); + glShaderSource(f, 1, &pFragmentShaderSrc, NULL); GLint compiled; glCompileShader(v); glGetShaderiv(v, GL_COMPILE_STATUS, &compiled); - if (!compiled) - { + if (!compiled) { std::cerr << "Vertex shader not compiled." << std::endl; printShaderInfoLog(v); } glCompileShader(f); glGetShaderiv(f, GL_COMPILE_STATUS, &compiled); - if (!compiled) - { + if (!compiled) { std::cerr << "Fragment shader not compiled." << std::endl; printShaderInfoLog(f); } - shaders_t out; out.vertex = v; out.fragment = f; + Shaders out; out.vertex = v; out.fragment = f; return out; } -GLuint initShaders(const char* vshader_code, const char* fshader_code) +GLuint initShaders(const char* pVertShaderSrc, const char* pFragShaderSrc) { - shaders_t shaders = loadShaders(vshader_code, fshader_code); - GLuint shader_program = glCreateProgram(); - attachAndLinkProgram(shader_program, shaders); - return shader_program; + Shaders shrds = loadShaders(pVertShaderSrc, pFragShaderSrc); + GLuint shaderProgram = glCreateProgram(); + attachAndLinkProgram(shaderProgram, shrds); + return shaderProgram; } -int next_p2(int value) +int nextP2(const int pValue) { - return int(std::pow(2, (std::ceil(std::log2(value))))); + return int(std::pow(2, (std::ceil(std::log2(pValue))))); } -float clampTo01(float a) +float clampTo01(const float pValue) { - return (a < 0.0f ? 0.0f : (a>1.0f ? 1.0f : a)); + return (pValue < 0.0f ? 0.0f : (pValue>1.0f ? 1.0f : pValue)); } #ifdef OS_WIN #include #include -void getFontFilePaths(std::vector& pFiles, std::string pDir, std::string pExt) +void getFontFilePaths(std::vector& pFiles, + const std::string& pDir, + const std::string& pExt) { WIN32_FIND_DATA ffd; LARGE_INTEGER filesize; @@ -249,9 +228,62 @@ void getFontFilePaths(std::vector& pFiles, std::string pDir, std::s } #endif -std::string toString(float pVal, const int n) +std::string toString(const float pVal, const int pPrecision) { std::ostringstream out; - out << std::fixed << std::setprecision(n) << pVal; + out << std::fixed << std::setprecision(pPrecision) << pVal; return out.str(); } + +GLuint screenQuadVBO(const int pWindowId) +{ + //FIXME: VBOs can be shared, but for simplicity + // right now just created one VBO each window, + // ignoring shared contexts + static std::map svboMap; + + if (svboMap.find(pWindowId)==svboMap.end()) { + static const float vertices[8] = { + -1.0f,-1.0f, + 1.0f,-1.0f, + 1.0f, 1.0f, + -1.0f, 1.0f + }; + svboMap[pWindowId] = createBuffer(GL_ARRAY_BUFFER, 8, vertices, GL_STATIC_DRAW); + } + + return svboMap[pWindowId]; +} + +GLuint screenQuadVAO(const int pWindowId) +{ + static std::map svaoMap; + + if (svaoMap.find(pWindowId)==svaoMap.end()) { + static const float texcords[8] = {0.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0}; + static const uint indices[6] = {0,1,2,0,2,3}; + + GLuint tbo = createBuffer(GL_ARRAY_BUFFER, 8, texcords, GL_STATIC_DRAW); + GLuint ibo = createBuffer(GL_ELEMENT_ARRAY_BUFFER, 6, indices, GL_STATIC_DRAW); + + GLuint vao = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + // attach vbo + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, screenQuadVBO(pWindowId)); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, NULL); + // attach tbo + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, tbo); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, NULL); + // attach ibo + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); + glBindVertexArray(0); + /* store the vertex array object corresponding to + * the window instance in the map */ + svaoMap[pWindowId] = vao; + } + + return svaoMap[pWindowId]; +} diff --git a/src/common.hpp b/src/common.hpp index b108c4a5..3e96befa 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -9,10 +9,13 @@ #pragma once -#define GLM_FORCE_RADIANS #include #include #include + +#define GLM_FORCE_RADIANS +#include + #include static const float GRAY[] = {0.0f , 0.0f , 0.0f , 1.0f}; @@ -20,6 +23,109 @@ static const float WHITE[] = {1.0f , 1.0f , 1.0f , 1.0f}; static const float BLUE[] = {0.0588f, 0.1137f, 0.2745f, 1.0f}; static const float RED[] = {1.0f , 0.0f , 0.0f , 1.0f}; +/* clamp the float to [0-1] range + * + * @pValue is the value to be clamped + */ +float clampTo01(const float pValue); + +/* Convert forge type enum to OpenGL enum for GL_* type + * + * @pValue is the forge type enum + * + * @return GL_* typedef for data type + */ +GLenum dtype2gl(const fg::dtype pValue); + +/* Convert forge channel format enum to OpenGL enum to indicate color component layout + * + * @pValue is the forge type enum + * + * @return OpenGL enum indicating color component layout + */ +GLenum ctype2gl(const fg::ChannelFormat pMode); + +/* Convert forge channel format enum to OpenGL enum to indicate color component layout + * + * This function is used to group color component layout formats based + * on number of components. + * + * @pValue is the forge type enum + * + * @return OpenGL enum indicating color component layout + */ +GLenum ictype2gl(const fg::ChannelFormat pMode); + +/* Compile OpenGL GLSL vertex and fragment shader sources + * + * @pVertShaderSrc is the vertex shader source code string + * @pFragShaderSrc is the vertex shader source code string + * + * @return GLSL program unique identifier for given shader duo + */ +GLuint initShaders(const char* pVertShaderSrc, const char* pFragShaderSrc); + +/* Create OpenGL buffer object + * + * @pTarget should be either GL_ARRAY_BUFFER or GL_ELEMENT_ARRAY_BUFFER + * @pSize is the size of the data in bytes + * @pPtr is the pointer to host data. This can be NULL + * @pUsage should be either GL_STATIC_DRAW or GL_DYNAMIC_DRAW + * + * @return OpenGL buffer object identifier + */ +template +GLuint createBuffer(GLenum pTarget, size_t pSize, const T* pPtr, GLenum pUsage) +{ + GLuint retVal = 0; + glGenBuffers(1, &retVal); + glBindBuffer(pTarget, retVal); + glBufferData(pTarget, pSize*sizeof(T), pPtr, pUsage); + glBindBuffer(pTarget, 0); + return retVal; +} + +/* compute the next power of two after given integer + * + * @pValue is the value whose next power of two is to be computed + */ +int nextP2(const int pValue); + +#ifdef OS_WIN +/* Get the paths to font files in Windows system directory + * + * @pFiles is the output vector to which font file paths are appended to. + * @pDir is the directory from which font files are looked up + * @pExt is the target font file extension we are looking for. + */ +void getFontFilePaths(std::vector& pFiles, + const std::string& pDir, + const std::string& pExt); +#endif + +/* Convert float value to string with given precision + * + * @pVal is the float value whose string representation is requested. + * @pPrecision is the precision of the float used while converting to string. + * + * @return is the string representation of input float value. + */ +std::string toString(const float pVal, const int pPrecision = 2); + +/* Get a vertex buffer object for quad that spans the screen + */ +GLuint screenQuadVBO(const int pWindowId); + +/* Get a vertex array object that uses screenQuadVBO + * + * This vertex array object when bound and rendered, basically + * draws a rectangle over the entire screen with standard + * texture coordinates. Use of this vao would be as follows + * + * `glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);` + */ +GLuint screenQuadVAO(const int pWindowId); + namespace internal { @@ -29,57 +135,84 @@ namespace internal * class. */ class AbstractRenderable { + protected: + /* OpenGL buffer objects */ + GLuint mVBO; + GLuint mCBO; + GLuint mABO; + size_t mVBOSize; + size_t mCBOSize; + size_t mABOSize; + GLfloat mColor[4]; + GLfloat mRange[6]; + std::string mLegend; + public: + /* Getter functions for OpenGL buffer objects + * identifiers and their size in bytes + * + * vbo is for vertices + * cbo is for colors of those vertices + * abo is for alpha values for those vertices + */ + virtual GLuint vbo() const { return mVBO; } + virtual GLuint cbo() const { return mCBO; } + virtual GLuint abo() const { return mABO; } + virtual size_t vboSize() const { return mVBOSize; } + virtual size_t cboSize() const { return mCBOSize; } + virtual size_t aboSize() const { return mABOSize; } + + /* Set color for rendering + */ + virtual void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { + mColor[0] = clampTo01(pRed); + mColor[1] = clampTo01(pGreen); + mColor[2] = clampTo01(pBlue); + mColor[3] = clampTo01(pAlpha); + } + + /* Set legend for rendering + */ + virtual void setLegend(const std::string& pLegend) { + mLegend = pLegend; + } + + /* Set 3d world coordinate ranges + * + * This method is mostly used for charts and related renderables + */ + virtual void setRanges(const float pMinX, const float pMaxX, + const float pMinY, const float pMaxY, + const float pMinZ, const float pMaxZ) { + mRange[0] = pMinX; mRange[1] = pMaxX; + mRange[2] = pMinY; mRange[3] = pMaxY; + mRange[4] = pMinZ; mRange[5] = pMaxZ; + } + + /* virtual function to set colormap, a derviced class might + * use it or ignore it if it doesnt have a need for color maps. + */ + virtual void setColorMapUBOParams(const GLuint pUBO, const GLuint pSize) { + } + /* render is a pure virtual function. - * @pX X coordinate at which the currently bound viewport begins. - * @pX Y coordinate at which the currently bound viewport begins. - * @pViewPortWidth Width of the currently bound viewport. - * @pViewPortHeight Height of the currently bound viewport. + * + * @pWindowId is the window identifier + * @pX is the X coordinate at which the currently bound viewport begins. + * @pX is the Y coordinate at which the currently bound viewport begins. + * @pViewPortWidth is the width of the currently bound viewport. + * @pViewPortHeight is the height of the currently bound viewport. * * Any concrete class that inherits AbstractRenderable class needs to * implement this method to render their OpenGL objects to - * the currently bound viewport. + * the currently bound viewport of the Window bearing identifier pWindowId. * * @return nothing. */ - virtual void render(int pWindowId, - int pX, int pY, int pViewPortWidth, int pViewPortHeight) = 0; - - /* virtual function to set colormap, a derviced class might - * use it or ignore it if it doesnt have a need for color maps */ - virtual void setColorMapUBOParams(GLuint ubo, GLuint size) { - } + virtual void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4 &pTransform) = 0; }; } - -GLenum gl_dtype(fg::dtype val); - -GLenum gl_ctype(fg::ChannelFormat mode); - -GLenum gl_ictype(fg::ChannelFormat mode); - -char* loadFile(const char *fname, GLint &fSize); - -GLuint initShaders(const char* vshader_code, const char* fshader_code); - -template -GLuint createBuffer(GLenum target, size_t size, const T* data, GLenum usage) -{ - GLuint ret_val = 0; - glGenBuffers(1, &ret_val); - glBindBuffer(target, ret_val); - glBufferData(target, size*sizeof(T), data, usage); - glBindBuffer(target, 0); - return ret_val; -} - -int next_p2(int value); - -float clampTo01(float a); - -#ifdef OS_WIN -void getFontFilePaths(std::vector& pFiles, std::string pDir, std::string pExt); -#endif - -std::string toString(float pVal, const int n = 2); diff --git a/src/exception.cpp b/src/exception.cpp index a319a029..79c5b462 100644 --- a/src/exception.cpp +++ b/src/exception.cpp @@ -20,7 +20,7 @@ using std::cerr; std::string getName(fg::dtype pType) { - // FIXME + // TODO return std::string("test"); } diff --git a/src/font.cpp b/src/font.cpp index 0592b746..82ba0d9a 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include @@ -34,32 +36,6 @@ static FT_Library gFTLib; static FT_Face gFTFace; -static const char* gFontVertShader = -"#version 330\n" -"uniform mat4 projectionMatrix;\n" -"uniform mat4 modelViewMatrix;\n" -"layout (location = 0) in vec2 inPosition;\n" -"layout (location = 1) in vec2 inCoord;\n" -"out vec2 texCoord;\n" -"void main()\n" -"{\n" -" gl_Position = projectionMatrix*modelViewMatrix*vec4(inPosition, 0.0, 1.0);\n" -" texCoord = inCoord;\n" -"}\n"; - -static const char* gFontFragShader = -"#version 330\n" -"in vec2 texCoord;\n" -"out vec4 outputColor;\n" -"uniform sampler2D tex;\n" -"uniform vec4 textColor;\n" -"void main()\n" -"{\n" -" vec4 texC = texture(tex, texCoord);\n" -" vec4 alpha = vec4(1.0, 1.0, 1.0, texC.r);\n" -" outputColor = alpha*textColor;\n" -"}\n"; - static const int START_CHAR = 32; static const int END_CHAR = 127; @@ -79,8 +55,8 @@ void font_impl::extractGlyph(int pCharacter) int bmp_w = bitmap.width; int bmp_h = bitmap.rows; - int w = next_p2(bmp_w); - int h = next_p2(bmp_h); + int w = nextP2(bmp_w); + int h = nextP2(bmp_h); std::vector glyphData(w*h, 0); for (int j=0; j +#include #include #include +#include +#include #include #include @@ -19,40 +22,10 @@ using namespace std; -const char *gHistBarVertexShaderSrc = -"#version 330\n" -"in vec2 point;\n" -"in float freq;\n" -"uniform float ymax;\n" -"uniform float nbins;\n" -"uniform mat4 transform;\n" -"void main(void) {\n" -" float binId = gl_InstanceID;\n" -" float deltax = 2.0f/nbins;\n" -" float deltay = 2.0f/ymax;\n" -" float xcurr = -1.0f + binId * deltax;\n" -" if (point.x==1) {\n" -" xcurr += deltax;\n" -" }\n" -" float ycurr = -1.0f;\n" -" if (point.y==1) {\n" -" ycurr += deltay * freq;\n" -" }\n" -" gl_Position = transform * vec4(xcurr, ycurr, 0, 1);\n" -"}"; - -const char *gHistBarFragmentShaderSrc = -"#version 330\n" -"uniform vec4 barColor;\n" -"out vec4 outColor;\n" -"void main(void) {\n" -" outColor = barColor;\n" -"}"; - namespace internal { -void hist_impl::bindResources(int pWindowId) +void hist_impl::bindResources(const int pWindowId) { if (mVAOMap.find(pWindowId) == mVAOMap.end()) { GLuint vao = 0; @@ -63,12 +36,20 @@ void hist_impl::bindResources(int pWindowId) glEnableVertexAttribArray(mPointIndex); glEnableVertexAttribArray(mFreqIndex); // attach histogram bar vertices - glBindBuffer(GL_ARRAY_BUFFER, mDecorVBO); + glBindBuffer(GL_ARRAY_BUFFER, screenQuadVBO(pWindowId)); glVertexAttribPointer(mPointIndex, 2, GL_FLOAT, GL_FALSE, 0, 0); // attach histogram frequencies - glBindBuffer(GL_ARRAY_BUFFER, mHistogramVBO); + glBindBuffer(GL_ARRAY_BUFFER, mVBO); glVertexAttribPointer(mFreqIndex, 1, mGLType, GL_FALSE, 0, 0); glVertexAttribDivisor(mFreqIndex, 1); + // attach histogram bar colors + glBindBuffer(GL_ARRAY_BUFFER, mCBO); + glVertexAttribPointer(mColorIndex, 3, GL_FLOAT, GL_FALSE, 0, 0); + glVertexAttribDivisor(mColorIndex, 1); + // attach histogram bar alphas + glBindBuffer(GL_ARRAY_BUFFER, mABO); + glVertexAttribPointer(mAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); + glVertexAttribDivisor(mAlphaIndex, 1); glBindVertexArray(0); /* store the vertex array object corresponding to * the window instance in the map */ @@ -80,53 +61,58 @@ void hist_impl::bindResources(int pWindowId) void hist_impl::unbindResources() const { + glVertexAttribDivisor(mFreqIndex, 0); glBindVertexArray(0); - //glVertexAttribDivisor(mFreqIndex, 0); } -hist_impl::hist_impl(unsigned pNBins, fg::dtype pDataType) - : Chart2D(), mDataType(pDataType), mGLType(gl_dtype(mDataType)), - mNBins(pNBins), mHistogramVBO(0), mHistogramVBOSize(0), mHistBarProgram(0), - mHistBarMatIndex(0), mHistBarColorIndex(0), mHistBarYMaxIndex(0), - mPointIndex(0), mFreqIndex(0) +hist_impl::hist_impl(const uint pNBins, const fg::dtype pDataType) + : mDataType(pDataType), mGLType(dtype2gl(mDataType)), mNBins(pNBins), + mIsPVCOn(0), mProgram(0), mYMaxIndex(-1), mNBinsIndex(-1), + mMatIndex(-1), mPointIndex(-1), mFreqIndex(-1), mColorIndex(-1), + mAlphaIndex(-1), mPVCIndex(-1), mBColorIndex(-1) { - CheckGL("Begin hist_impl::hist_impl"); - mHistBarProgram = initShaders(gHistBarVertexShaderSrc, gHistBarFragmentShaderSrc); + mColor[0] = 0.8f; + mColor[1] = 0.6f; + mColor[2] = 0.0f; + mColor[3] = 1.0f; + mLegend = std::string(""); - mPointIndex = glGetAttribLocation (mHistBarProgram, "point"); - mFreqIndex = glGetAttribLocation (mHistBarProgram, "freq"); - mHistBarColorIndex = glGetUniformLocation(mHistBarProgram, "barColor"); - mHistBarMatIndex = glGetUniformLocation(mHistBarProgram, "transform"); - mHistBarNBinsIndex = glGetUniformLocation(mHistBarProgram, "nbins"); - mHistBarYMaxIndex = glGetUniformLocation(mHistBarProgram, "ymax"); + CheckGL("Begin hist_impl::hist_impl"); + mProgram = initShaders(glsl::histogram_vs.c_str(), glsl::histogram_fs.c_str()); + + mYMaxIndex = glGetUniformLocation(mProgram, "ymax" ); + mNBinsIndex = glGetUniformLocation(mProgram, "nbins" ); + mMatIndex = glGetUniformLocation(mProgram, "transform"); + mPointIndex = glGetAttribLocation (mProgram, "point" ); + mFreqIndex = glGetAttribLocation (mProgram, "freq" ); + mColorIndex = glGetUniformLocation(mProgram, "color" ); + mAlphaIndex = glGetAttribLocation (mProgram, "alpha" ); + mPVCIndex = glGetUniformLocation(mProgram, "isPVCOn" ); + mBColorIndex = glGetAttribLocation (mProgram, "barColor" ); + + mVBOSize = mNBins; + mCBOSize = 3*mVBOSize; + mABOSize = mNBins; + +#define HIST_CREATE_BUFFERS(type) \ + mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ + mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ + mABO = createBuffer(GL_ARRAY_BUFFER, mABOSize, NULL, GL_DYNAMIC_DRAW); \ + mVBOSize *= sizeof(type); \ + mCBOSize *= sizeof(float); \ + mABOSize *= sizeof(float); switch(mGLType) { - case GL_FLOAT: - mHistogramVBO = createBuffer(GL_ARRAY_BUFFER, mNBins, NULL, GL_DYNAMIC_DRAW); - mHistogramVBOSize = mNBins*sizeof(float); - break; - case GL_INT: - mHistogramVBO = createBuffer(GL_ARRAY_BUFFER, mNBins, NULL, GL_DYNAMIC_DRAW); - mHistogramVBOSize = mNBins*sizeof(int); - break; - case GL_UNSIGNED_INT: - mHistogramVBO = createBuffer(GL_ARRAY_BUFFER, mNBins, NULL, GL_DYNAMIC_DRAW); - mHistogramVBOSize = mNBins*sizeof(unsigned); - break; - case GL_SHORT: - mHistogramVBO = createBuffer(GL_ARRAY_BUFFER, mNBins, NULL, GL_DYNAMIC_DRAW); - mHistogramVBOSize = mNBins*sizeof(short); - break; - case GL_UNSIGNED_SHORT: - mHistogramVBO = createBuffer(GL_ARRAY_BUFFER, mNBins, NULL, GL_DYNAMIC_DRAW); - mHistogramVBOSize = mNBins*sizeof(unsigned short); - break; - case GL_UNSIGNED_BYTE: - mHistogramVBO = createBuffer(GL_ARRAY_BUFFER, mNBins, NULL, GL_DYNAMIC_DRAW); - mHistogramVBOSize = mNBins*sizeof(unsigned char); - break; - default: fg::TypeError("Plot::Plot", __LINE__, 1, mDataType); + case GL_FLOAT : HIST_CREATE_BUFFERS(float) ; break; + case GL_INT : HIST_CREATE_BUFFERS(int) ; break; + case GL_UNSIGNED_INT : HIST_CREATE_BUFFERS(uint) ; break; + case GL_SHORT : HIST_CREATE_BUFFERS(short) ; break; + case GL_UNSIGNED_SHORT : HIST_CREATE_BUFFERS(ushort); break; + case GL_UNSIGNED_BYTE : HIST_CREATE_BUFFERS(float) ; break; + default: fg::TypeError("hist_impl::hist_impl", __LINE__, 1, mDataType); } +#undef HIST_CREATE_BUFFERS + CheckGL("End hist_impl::hist_impl"); } @@ -137,55 +123,27 @@ hist_impl::~hist_impl() GLuint vao = it->second; glDeleteVertexArrays(1, &vao); } - glDeleteBuffers(1, &mHistogramVBO); - glDeleteProgram(mHistBarProgram); + glDeleteBuffers(1, &mVBO); + glDeleteBuffers(1, &mCBO); + glDeleteBuffers(1, &mABO); + glDeleteProgram(mProgram); CheckGL("End hist_impl::~hist_impl"); } -void hist_impl::setBarColor(float r, float g, float b) -{ - mBarColor[0] = r; - mBarColor[1] = g; - mBarColor[2] = b; - mBarColor[3] = 1.0f; -} - -GLuint hist_impl::vbo() const -{ - return mHistogramVBO; -} - -size_t hist_impl::size() const +void hist_impl::render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) { - return mHistogramVBOSize; -} - -void hist_impl::render(int pWindowId, int pX, int pY, int pVPW, int pVPH) -{ - float w = float(pVPW - (mLeftMargin+mRightMargin+mTickSize)); - float h = float(pVPH - (mBottomMargin+mTopMargin+mTickSize)); - float offset_x = (2.0f * (mLeftMargin+mTickSize) + (w - pVPW)) / pVPW; - float offset_y = (2.0f * (mBottomMargin+mTickSize) + (h - pVPH)) / pVPH; - float scale_x = w / pVPW; - float scale_y = h / pVPH; - - CheckGL("Begin Histogram::render"); - /* Enavle scissor test to discard anything drawn beyond viewport. - * Set scissor rectangle to clip fragments outside of viewport */ - glScissor(pX+mLeftMargin+mTickSize, pY+mBottomMargin+mTickSize, - pVPW - (mLeftMargin+mRightMargin+mTickSize), - pVPH - (mBottomMargin+mTopMargin+mTickSize)); + CheckGL("Begin hist_impl::render"); + glScissor(pX, pY, pVPW, pVPH); glEnable(GL_SCISSOR_TEST); + glUseProgram(mProgram); - glm::mat4 trans = glm::translate(glm::scale(glm::mat4(1), - glm::vec3(scale_x, scale_y, 1)), - glm::vec3(offset_x, offset_y, 0)); - - glUseProgram(mHistBarProgram); - glUniformMatrix4fv(mHistBarMatIndex, 1, GL_FALSE, glm::value_ptr(trans)); - glUniform4fv(mHistBarColorIndex, 1, mBarColor); - glUniform1f(mHistBarNBinsIndex, (GLfloat)mNBins); - glUniform1f(mHistBarYMaxIndex, ymax()); + glUniform1f(mYMaxIndex, mRange[3]); + glUniform1f(mNBinsIndex, (GLfloat)mNBins); + glUniformMatrix4fv(mMatIndex, 1, GL_FALSE, glm::value_ptr(pTransform)); + glUniform1i(mPVCIndex, mIsPVCOn); + glUniform4fv(mColorIndex, 1, mColor); /* render a rectangle for each bin. Same * rectangle is scaled and translated accordingly @@ -196,11 +154,8 @@ void hist_impl::render(int pWindowId, int pX, int pY, int pVPW, int pVPH) hist_impl::unbindResources(); glUseProgram(0); - /* Stop clipping */ glDisable(GL_SCISSOR_TEST); - - renderChart(pWindowId, pX, pY, pVPW, pVPH); - CheckGL("End Histogram::render"); + CheckGL("End hist_impl::render"); } } @@ -208,78 +163,74 @@ void hist_impl::render(int pWindowId, int pX, int pY, int pVPW, int pVPH) namespace fg { -Histogram::Histogram(unsigned pNBins, fg::dtype pDataType) +Histogram::Histogram(const uint pNBins, const dtype pDataType) { - value = new internal::_Histogram(pNBins, pDataType); + mValue = new internal::_Histogram(pNBins, pDataType); } -Histogram::Histogram(const Histogram& other) +Histogram::Histogram(const Histogram& pOther) { - value = new internal::_Histogram(*other.get()); + mValue = new internal::_Histogram(*pOther.get()); } Histogram::~Histogram() { - delete value; -} - -void Histogram::setBarColor(fg::Color col) -{ - float r = (((int) col >> 24 ) & 0xFF ) / 255.f; - float g = (((int) col >> 16 ) & 0xFF ) / 255.f; - float b = (((int) col >> 8 ) & 0xFF ) / 255.f; - // float a = (((int) col ) & 0xFF ) / 255.f; - value->setBarColor(r, g, b); + delete mValue; } -void Histogram::setBarColor(float r, float g, float b) +void Histogram::setColor(const Color pColor) { - value->setBarColor(r, g, b); + float r = (((int) pColor >> 24 ) & 0xFF ) / 255.f; + float g = (((int) pColor >> 16 ) & 0xFF ) / 255.f; + float b = (((int) pColor >> 8 ) & 0xFF ) / 255.f; + float a = (((int) pColor ) & 0xFF ) / 255.f; + mValue->setColor(r, g, b, a); } -void Histogram::setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin) +void Histogram::setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { - value->setAxesLimits(pXmax, pXmin, pYmax, pYmin); + mValue->setColor(pRed, pGreen, pBlue, pAlpha); } -void Histogram::setAxesTitles(const char* pXTitle, const char* pYTitle) +void Histogram::setLegend(const std::string pLegend) { - value->setAxesTitles(pXTitle, pYTitle); + mValue->setLegend(pLegend); } -float Histogram::xmax() const +uint Histogram::vertices() const { - return value->xmax(); + return mValue->vbo(); } -float Histogram::xmin() const +uint Histogram::colors() const { - return value->xmin(); + return mValue->cbo(); } -float Histogram::ymax() const +uint Histogram::alphas() const { - return value->ymax(); + return mValue->abo(); } -float Histogram::ymin() const +uint Histogram::verticesSize() const { - return value->ymin(); + return (uint)mValue->vboSize(); } -unsigned Histogram::vbo() const +uint Histogram::colorsSize() const { - return value->vbo(); + return (uint)mValue->cboSize(); } -unsigned Histogram::size() const +uint Histogram::alphasSize() const { - return (unsigned)value->size(); + return (uint)mValue->aboSize(); } internal::_Histogram* Histogram::get() const { - return value; + return mValue; } } diff --git a/src/histogram.hpp b/src/histogram.hpp index 38f6cb38..33932d51 100644 --- a/src/histogram.hpp +++ b/src/histogram.hpp @@ -10,98 +10,103 @@ #pragma once #include -#include + #include #include namespace internal { -class hist_impl : public Chart2D { +class hist_impl : public AbstractRenderable { private: /* plot points characteristics */ fg::dtype mDataType; GLenum mGLType; GLuint mNBins; - float mBarColor[4]; + bool mIsPVCOn; /* OpenGL Objects */ - GLuint mHistogramVBO; - size_t mHistogramVBOSize; - GLuint mHistBarProgram; - /* internal shader attributes for mHistBarProgram + GLuint mProgram; + /* internal shader attributes for mProgram * shader program to render histogram bars for each * bin*/ - GLuint mHistBarMatIndex; - GLuint mHistBarColorIndex; - GLuint mHistBarNBinsIndex; - GLuint mHistBarYMaxIndex; + GLuint mYMaxIndex; + GLuint mNBinsIndex; + GLuint mMatIndex; GLuint mPointIndex; GLuint mFreqIndex; + GLuint mColorIndex; + GLuint mAlphaIndex; + GLuint mPVCIndex; + GLuint mBColorIndex; + + float mRange[6]; std::map mVAOMap; /* bind and unbind helper functions * for rendering resources */ - void bindResources(int pWindowId); + void bindResources(const int pWindowId); void unbindResources() const; + void computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput); public: - hist_impl(unsigned pNBins, fg::dtype pDataType); + hist_impl(const uint pNBins, const fg::dtype pDataType); ~hist_impl(); - void setBarColor(float r, float g, float b); - GLuint vbo() const; - size_t size() const; - - void render(int pWindowId, int pX, int pY, int pViewPortWidth, int pViewPortHeight); + void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform); }; class _Histogram { private: - std::shared_ptr hst; + std::shared_ptr mHistogram; public: - _Histogram(unsigned pNBins, fg::dtype pDataType) - : hst(std::make_shared(pNBins, pDataType)) {} + _Histogram(uint pNBins, fg::dtype pDataType) + : mHistogram(std::make_shared(pNBins, pDataType)) {} inline const std::shared_ptr& impl() const { - return hst; + return mHistogram; } - inline void setBarColor(float r, float g, float b) { - hst->setBarColor(r, g, b); + inline void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { + mHistogram->setColor(pRed, pGreen, pBlue, pAlpha); } - inline void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin) { - hst->setAxesLimits(pXmax, pXmin, pYmax, pYmin); + inline void setLegend(const std::string pLegend) { + mHistogram->setLegend(pLegend); } - inline void setAxesTitles(const char* pXTitle, const char* pYTitle) { - hst->setAxesTitles(pXTitle, pYTitle); + inline GLuint vbo() const { + return mHistogram->vbo(); } - inline float xmax() const { - return hst->xmax(); + inline GLuint cbo() const { + return mHistogram->cbo(); } - inline float xmin() const { - return hst->xmin(); + inline GLuint abo() const { + return mHistogram->abo(); } - inline float ymax() const { - return hst->ymax(); + inline size_t vboSize() const { + return mHistogram->vboSize(); } - inline float ymin() const { - return hst->ymin(); + inline size_t cboSize() const { + return mHistogram->cboSize(); } - inline GLuint vbo() const { - return hst->vbo(); + inline size_t aboSize() const { + return mHistogram->aboSize(); } - inline size_t size() const { - return hst->size(); + inline void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) const { + mHistogram->render(pWindowId, pX, pY, pVPW, pVPH, pTransform); } }; diff --git a/src/image.cpp b/src/image.cpp index f00019c5..fe3a8d29 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -8,97 +8,26 @@ ********************************************************/ #include +#include #include +#include #include -#include -#include +#include +#include #include #include #include -static const char* vertex_shader_code = -"#version 330\n" -"layout(location = 0) in vec3 pos;\n" -"layout(location = 1) in vec2 tex;\n" -"uniform mat4 matrix;\n" -"out vec2 texcoord;\n" -"void main() {\n" -" texcoord = tex;\n" -" gl_Position = matrix * vec4(pos,1.0);\n" -"}\n"; - -static const char* fragment_shader_code = -"#version 330\n" -"const int size = 259;\n" -"uniform float cmaplen;\n" -"layout(std140) uniform ColorMap\n" -"{\n" -" vec4 ch[size];\n" -"};\n" -"uniform sampler2D tex;\n" -"uniform bool isGrayScale;\n" -"in vec2 texcoord;\n" -"out vec4 fragColor;\n" -"void main()\n" -"{\n" -" vec4 tcolor = texture(tex, texcoord);\n" -" vec4 clrs = vec4(1, 0, 0, 1);\n" -" if(isGrayScale)\n" -" clrs = vec4(tcolor.r, tcolor.r, tcolor.r, 1);\n" -" else\n" -" clrs = tcolor;\n" -" vec4 fidx = (cmaplen-1) * clrs;\n" -" ivec4 idx = ivec4(fidx.x, fidx.y, fidx.z, fidx.w);\n" -" float r_ch = ch[idx.x].r;\n" -" float g_ch = ch[idx.y].g;\n" -" float b_ch = ch[idx.z].b;\n" -" fragColor = vec4(r_ch, g_ch , b_ch, 1);\n" -"}\n"; - -GLuint imageQuadVAO(int pWindowId) -{ - static std::map mVAOMap; - - if (mVAOMap.find(pWindowId)==mVAOMap.end()) { - static const float vertices[12] = {-1.0f,-1.0f,0.0, - 1.0f,-1.0f,0.0, - 1.0f, 1.0f,0.0, - -1.0f, 1.0f,0.0}; - static const float texcords[8] = {0.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0}; - static const unsigned indices[6]= {0,1,2,0,2,3}; - GLuint vbo = createBuffer(GL_ARRAY_BUFFER, 12, vertices, GL_STATIC_DRAW); - GLuint tbo = createBuffer(GL_ARRAY_BUFFER, 8, texcords, GL_STATIC_DRAW); - GLuint ibo = createBuffer(GL_ELEMENT_ARRAY_BUFFER, 6, indices, GL_STATIC_DRAW); - - GLuint vao = 0; - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - glEnableVertexAttribArray(0); - glEnableVertexAttribArray(1); - // attach vbo - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL); - // attach tbo - glBindBuffer(GL_ARRAY_BUFFER, tbo); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, NULL); - // attach ibo - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); - glBindVertexArray(0); - /* store the vertex array object corresponding to - * the window instance in the map */ - mVAOMap[pWindowId] = vao; - } - - return mVAOMap[pWindowId]; -} +#include +#include namespace internal { -void image_impl::bindResources(int pWindowId) +void image_impl::bindResources(int pWindowId) const { - glBindVertexArray(imageQuadVAO(pWindowId)); + glBindVertexArray(screenQuadVAO(pWindowId)); } void image_impl::unbindResources() const @@ -106,14 +35,22 @@ void image_impl::unbindResources() const glBindVertexArray(0); } -image_impl::image_impl(unsigned pWidth, unsigned pHeight, - fg::ChannelFormat pFormat, fg::dtype pDataType) - : mWidth(pWidth), mHeight(pHeight), - mFormat(pFormat), mGLformat(gl_ctype(mFormat)), mGLiformat(gl_ictype(mFormat)), - mDataType(pDataType), mGLType(gl_dtype(mDataType)) +image_impl::image_impl(const uint pWidth, const uint pHeight, + const fg::ChannelFormat pFormat, const fg::dtype pDataType) + : mWidth(pWidth), mHeight(pHeight), mFormat(pFormat), + mGLformat(ctype2gl(mFormat)), mGLiformat(ictype2gl(mFormat)), + mDataType(pDataType), mGLType(dtype2gl(mDataType)), mAlpha(1.0f), mKeepARatio(true), + mMatIndex(-1), mTexIndex(-1), mIsGrayIndex(-1), mCMapLenIndex(-1), mCMapIndex(-1) { CheckGL("Begin image_impl::image_impl"); + mProgram = initShaders(glsl::image_vs.c_str(), glsl::image_fs.c_str()); + mMatIndex = glGetUniformLocation(mProgram, "matrix"); + mCMapIndex = glGetUniformBlockIndex(mProgram, "ColorMap"); + mCMapLenIndex = glGetUniformLocation(mProgram, "cmaplen"); + mTexIndex = glGetUniformLocation(mProgram, "tex"); + mIsGrayIndex = glGetUniformLocation(mProgram, "isGrayScale"); + // Initialize OpenGL Items glGenTextures(1, &(mTex)); glBindTexture(GL_TEXTURE_2D, mTex); @@ -129,22 +66,22 @@ image_impl::image_impl(unsigned pWidth, unsigned pHeight, glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mPBO); size_t typeSize = 0; switch(mGLType) { - case GL_INT: typeSize = sizeof(int ); break; - case GL_UNSIGNED_INT: typeSize = sizeof(unsigned int); break; - case GL_SHORT: typeSize = sizeof(short ); break; - case GL_UNSIGNED_SHORT: typeSize = sizeof(unsigned short); break; - case GL_BYTE: typeSize = sizeof(char ); break; - case GL_UNSIGNED_BYTE: typeSize = sizeof(unsigned char); break; + case GL_INT: typeSize = sizeof(int ); break; + case GL_UNSIGNED_INT: typeSize = sizeof(uint ); break; + case GL_SHORT: typeSize = sizeof(short ); break; + case GL_UNSIGNED_SHORT: typeSize = sizeof(ushort); break; + case GL_BYTE: typeSize = sizeof(char ); break; + case GL_UNSIGNED_BYTE: typeSize = sizeof(uchar ); break; default: typeSize = sizeof(float); break; } size_t formatSize = 0; switch(mFormat) { - case fg::FG_GRAYSCALE: formatSize = 1; break; - case fg::FG_RG: formatSize = 2; break; - case fg::FG_RGB: formatSize = 3; break; - case fg::FG_BGR: formatSize = 3; break; - case fg::FG_RGBA: formatSize = 4; break; - case fg::FG_BGRA: formatSize = 4; break; + case fg::FG_GRAYSCALE: formatSize = 1; break; + case fg::FG_RG: formatSize = 2; break; + case fg::FG_RGB: formatSize = 3; break; + case fg::FG_BGR: formatSize = 3; break; + case fg::FG_RGBA: formatSize = 4; break; + case fg::FG_BGRA: formatSize = 4; break; default: formatSize = 1; break; } mPBOsize = mWidth * mHeight * formatSize * typeSize; @@ -152,9 +89,6 @@ image_impl::image_impl(unsigned pWidth, unsigned pHeight, glBindTexture(GL_TEXTURE_2D, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - CheckGL("After PBO Initialization"); - - mProgram = initShaders(vertex_shader_code, fragment_shader_code); CheckGL("End image_impl::image_impl"); } @@ -168,60 +102,63 @@ image_impl::~image_impl() CheckGL("End image_impl::~image_impl"); } -void image_impl::setColorMapUBOParams(GLuint ubo, GLuint size) +void image_impl::setColorMapUBOParams(const GLuint pUBO, const GLuint pSize) { - mColorMapUBO = ubo; - mUBOSize = size; + mColorMapUBO = pUBO; + mUBOSize = pSize; } -void image_impl::keepAspectRatio(const bool keep) +void image_impl::setAlpha(const float pAlpha) { - mKeepARatio = keep; + mAlpha = pAlpha; } -unsigned image_impl::width() const { return mWidth; } +void image_impl::keepAspectRatio(const bool pKeep) +{ + mKeepARatio = pKeep; +} + +uint image_impl::width() const { return mWidth; } -unsigned image_impl::height() const { return mHeight; } +uint image_impl::height() const { return mHeight; } fg::ChannelFormat image_impl::pixelFormat() const { return mFormat; } fg::dtype image_impl::channelType() const { return mDataType; } -unsigned image_impl::pbo() const { return mPBO; } +uint image_impl::pbo() const { return mPBO; } -unsigned image_impl::size() const { return (unsigned)mPBOsize; } +uint image_impl::size() const { return (uint)mPBOsize; } -void image_impl::render(int pWindowId, int pX, int pY, int pViewPortWidth, int pViewPortHeight) +void image_impl::render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) { float xscale = 1.f; float yscale = 1.f; if (mKeepARatio) { if (mWidth > mHeight) { - float trgtH = pViewPortWidth * float(mHeight)/float(mWidth); + float trgtH = pVPW * float(mHeight)/float(mWidth); float trgtW = trgtH * float(mWidth)/float(mHeight); - xscale = trgtW/pViewPortWidth; - yscale = trgtH/pViewPortHeight; + xscale = trgtW/pVPW; + yscale = trgtH/pVPH; } else { - float trgtW = pViewPortHeight * float(mWidth)/float(mHeight); + float trgtW = pVPH * float(mWidth)/float(mHeight); float trgtH = trgtW * float(mHeight)/float(mWidth); - xscale = trgtW/pViewPortWidth; - yscale = trgtH/pViewPortHeight; + xscale = trgtW/pVPW; + yscale = trgtH/pVPH; } } - glm::mat4 strans = glm::scale(glm::mat4(1.0f), glm::vec3(xscale, yscale, 1)); + + glm::mat4 strans = glm::scale(pTransform, glm::vec3(xscale, yscale, 1)); glUseProgram(mProgram); - // get uniform locations - int mat_loc = glGetUniformLocation(mProgram, "matrix"); - int tex_loc = glGetUniformLocation(mProgram, "tex"); - int chn_loc = glGetUniformLocation(mProgram, "isGrayScale"); - int cml_loc = glGetUniformLocation(mProgram, "cmaplen"); - int ubo_idx = glGetUniformBlockIndex(mProgram, "ColorMap"); - - glUniform1i(chn_loc, mFormat==fg::FG_GRAYSCALE); + + glUniform1i(mIsGrayIndex, mFormat==fg::FG_GRAYSCALE); + // load texture from PBO glActiveTexture(GL_TEXTURE0); - glUniform1i(tex_loc, 0); + glUniform1i(mTexIndex, 0); glBindTexture(GL_TEXTURE_2D, mTex); // bind PBO to load data into texture glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mPBO); @@ -229,11 +166,11 @@ void image_impl::render(int pWindowId, int pX, int pY, int pViewPortWidth, int p glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mWidth, mHeight, mGLformat, mGLType, 0); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - glUniformMatrix4fv(mat_loc, 1, GL_FALSE, glm::value_ptr(strans)); + glUniformMatrix4fv(mMatIndex, 1, GL_FALSE, glm::value_ptr(strans)); - glUniform1f(cml_loc, (GLfloat)mUBOSize); + glUniform1f(mCMapLenIndex, (GLfloat)mUBOSize); glBindBufferBase(GL_UNIFORM_BUFFER, 0, mColorMapUBO); - glUniformBlockBinding(mProgram, ubo_idx, 0); + glUniformBlockBinding(mProgram, mCMapIndex, 0); CheckGL("Before render"); @@ -255,44 +192,79 @@ void image_impl::render(int pWindowId, int pX, int pY, int pViewPortWidth, int p namespace fg { -Image::Image(unsigned pWidth, unsigned pHeight, fg::ChannelFormat pFormat, fg::dtype pDataType) { - value = new internal::_Image(pWidth, pHeight, pFormat, pDataType); +Image::Image(const uint pWidth, const uint pHeight, + const ChannelFormat pFormat, const dtype pDataType) +{ + mValue = new internal::_Image(pWidth, pHeight, pFormat, pDataType); } -Image::Image(const Image& other) { - value = new internal::_Image(*other.get()); +Image::Image(const Image& pOther) +{ + mValue = new internal::_Image(*pOther.get()); } -Image::~Image() { - delete value; +Image::~Image() +{ + delete mValue; } -unsigned Image::width() const { - return value->width(); +void Image::setAlpha(const float pAlpha) +{ + mValue->setAlpha(pAlpha); +} + +void Image::keepAspectRatio(const bool pKeep) +{ + mValue->keepAspectRatio(pKeep); +} + +uint Image::width() const +{ + return mValue->width(); +} + +uint Image::height() const +{ + return mValue->height(); } -unsigned Image::height() const { - return value->height(); +ChannelFormat Image::pixelFormat() const +{ + return mValue->pixelFormat(); } -ChannelFormat Image::pixelFormat() const { - return value->pixelFormat(); +fg::dtype Image::channelType() const +{ + return mValue->channelType(); } -fg::dtype Image::channelType() const { - return value->channelType(); +GLuint Image::pbo() const +{ + return mValue->pbo(); } -GLuint Image::pbo() const { - return value->pbo(); +uint Image::size() const +{ + return (uint)mValue->size(); } -unsigned Image::size() const { - return (unsigned)value->size(); +void Image::render(const Window& pWindow, + const int pX, const int pY, const int pVPW, const int pVPH, + const std::vector& pTransform) const +{ + if (pTransform.size() < 16) { + throw ArgumentError("Image::render", __LINE__, 5, + "Insufficient transform matrix data"); + } + mValue->render(pWindow.get()->getID(), + pX, pY, pVPW, pVPH, + glm::make_mat4(pTransform.data())); } -internal::_Image* Image::get() const { - return value; + +internal::_Image* Image::get() const +{ + return mValue; } } diff --git a/src/image.hpp b/src/image.hpp index 4485c081..01024a50 100644 --- a/src/image.hpp +++ b/src/image.hpp @@ -10,6 +10,7 @@ #pragma once #include + #include namespace internal @@ -17,68 +18,87 @@ namespace internal class image_impl : public AbstractRenderable { private: - unsigned mWidth; - unsigned mHeight; + uint mWidth; + uint mHeight; fg::ChannelFormat mFormat; - GLenum mGLformat; - GLenum mGLiformat; + GLenum mGLformat; + GLenum mGLiformat; fg::dtype mDataType; - GLenum mGLType; + GLenum mGLType; + float mAlpha; + bool mKeepARatio; /* internal resources for interop */ - size_t mPBOsize; - GLuint mPBO; - GLuint mTex; - GLuint mProgram; - - GLuint mColorMapUBO; - GLuint mUBOSize; - bool mKeepARatio; + size_t mPBOsize; + GLuint mPBO; + GLuint mTex; + GLuint mProgram; + GLuint mMatIndex; + GLuint mTexIndex; + GLuint mIsGrayIndex; + GLuint mCMapLenIndex; + GLuint mCMapIndex; + /* color map details */ + GLuint mColorMapUBO; + GLuint mUBOSize; /* helper functions to bind and unbind * resources for render quad primitive */ - void bindResources(int pWindowId); + void bindResources(int pWindowId) const; void unbindResources() const; public: - image_impl(unsigned pWidth, unsigned pHeight, fg::ChannelFormat pFormat, fg::dtype pDataType); + image_impl(const uint pWidth, const uint pHeight, + const fg::ChannelFormat pFormat, const fg::dtype pDataType); ~image_impl(); - void setColorMapUBOParams(GLuint ubo, GLuint size); - void keepAspectRatio(const bool keep=true); + void setColorMapUBOParams(const GLuint pUBO, const GLuint pSize); + void setAlpha(const float pAlpha); + void keepAspectRatio(const bool pKeep=true); - unsigned width() const; - unsigned height() const; + uint width() const; + uint height() const; fg::ChannelFormat pixelFormat() const; fg::dtype channelType() const; - unsigned pbo() const; - unsigned size() const; + uint pbo() const; + uint size() const; - void render(int pWindowId, int pX, int pY, int pViewPortWidth, int pViewPortHeight); + void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform); }; class _Image { private: - std::shared_ptr img; + std::shared_ptr mImage; public: - _Image(unsigned pWidth, unsigned pHeight, fg::ChannelFormat pFormat, fg::dtype pDataType) - : img(std::make_shared(pWidth, pHeight, pFormat, pDataType)) {} + _Image(const uint pWidth, const uint pHeight, + const fg::ChannelFormat pFormat, const fg::dtype pDataType) + : mImage(std::make_shared(pWidth, pHeight, pFormat, pDataType)) {} + + inline const std::shared_ptr& impl() const { return mImage; } + + inline void setAlpha(const float pAlpha) { mImage->setAlpha(pAlpha); } - inline const std::shared_ptr& impl() const { return img; } + inline void keepAspectRatio(const bool pKeep) { mImage->keepAspectRatio(pKeep); } - inline void keepAspectRatio(const bool keep) { img->keepAspectRatio(keep); } + inline uint width() const { return mImage->width(); } - inline unsigned width() const { return img->width(); } + inline uint height() const { return mImage->height(); } - inline unsigned height() const { return img->height(); } + inline fg::ChannelFormat pixelFormat() const { return mImage->pixelFormat(); } - inline fg::ChannelFormat pixelFormat() const { return img->pixelFormat(); } + inline fg::dtype channelType() const { return mImage->channelType(); } - inline fg::dtype channelType() const { return img->channelType(); } + inline GLuint pbo() const { return mImage->pbo(); } - inline GLuint pbo() const { return img->pbo(); } + inline size_t size() const { return mImage->size(); } - inline size_t size() const { return img->size(); } + inline void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) const { + mImage->render(pWindowId, pX, pY, pVPW, pVPH, pTransform); + } }; } diff --git a/src/plot.cpp b/src/plot.cpp index 15383a69..1b5d9c79 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -9,320 +9,83 @@ #include #include -#include #include -#include -#include -#include - using namespace std; -static const char *gMarkerVertexShaderSrc = -"#version 330\n" -"in vec2 point;\n" -"uniform mat4 transform;\n" -"void main(void) {\n" -" gl_Position = transform * vec4(point.xy, 0, 1);\n" -" gl_PointSize = 10;\n" -"}"; - - -static const char *gMarkerSpriteFragmentShaderSrc = -"#version 330\n" -"uniform int marker_type;\n" -"uniform vec4 marker_color;\n" -"out vec4 outputColor;\n" -"void main(void) {\n" -" float dist = sqrt( (gl_PointCoord.x - 0.5) * (gl_PointCoord.x-0.5) + (gl_PointCoord.y-0.5) * (gl_PointCoord.y-0.5) );\n" -" bool in_bounds;\n" -" switch(marker_type) {\n" -" case 1:\n" -" in_bounds = dist < 0.3;\n" -" break;\n" -" case 2:\n" -" in_bounds = ( (dist > 0.3) && (dist<0.5) );\n" -" break;\n" -" case 3:\n" -" in_bounds = ((gl_PointCoord.x < 0.15) || (gl_PointCoord.x > 0.85)) ||\n" -" ((gl_PointCoord.y < 0.15) || (gl_PointCoord.y > 0.85));\n" -" break;\n" -" case 4:\n" -" in_bounds = (2*(gl_PointCoord.x - 0.25) - (gl_PointCoord.y + 0.5) < 0) && (2*(gl_PointCoord.x - 0.25) + (gl_PointCoord.y + 0.5) > 1);\n" -" break;\n" -" case 5:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.13 ||\n" -" abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.13 ;\n" -" break;\n" -" case 6:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5)) < 0.07 ||\n" -" abs((gl_PointCoord.y - 0.5)) < 0.07;\n" -" break;\n" -" case 7:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.07 ||\n" -" abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.07 ||\n" -" abs((gl_PointCoord.x - 0.5)) < 0.07 ||\n" -" abs((gl_PointCoord.y - 0.5)) < 0.07;\n" -" break;\n" -" default:\n" -" in_bounds = true;\n" -" }\n" -" if(!in_bounds)\n" -" discard;\n" -" else\n" -" outputColor = marker_color;\n" -"}"; - -namespace internal -{ - -void plot_impl::bindResources(int pWindowId) -{ - if (mVAOMap.find(pWindowId) == mVAOMap.end()) { - GLuint vao = 0; - /* create a vertex array object - * with appropriate bindings */ - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - // attach plot vertices - glEnableVertexAttribArray(mPointIndex); - glBindBuffer(GL_ARRAY_BUFFER, mMainVBO); - glVertexAttribPointer(mPointIndex, 2, mGLType, GL_FALSE, 0, 0); - glBindVertexArray(0); - /* store the vertex array object corresponding to - * the window instance in the map */ - mVAOMap[pWindowId] = vao; - } - - glBindVertexArray(mVAOMap[pWindowId]); -} - -void plot_impl::unbindResources() const -{ - glBindVertexArray(0); -} - -plot_impl::plot_impl(unsigned pNumPoints, fg::dtype pDataType, - fg::PlotType pPlotType, fg::MarkerType pMarkerType) - : Chart2D(), mNumPoints(pNumPoints), - mDataType(pDataType), mGLType(gl_dtype(mDataType)), - mMarkerType(pMarkerType), mPlotType(pPlotType), - mMainVBO(0), mMainVBOsize(0), mPointIndex(0) -{ - mMarkerProgram = initShaders(gMarkerVertexShaderSrc, gMarkerSpriteFragmentShaderSrc); - mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); - mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); - mSpriteTMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); - mPointIndex = mBorderAttribPointIndex; - - unsigned total_points = 2*mNumPoints; - // buffersubdata calls on mMainVBO - // will only update the points data - switch(mGLType) { - case GL_FLOAT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(float); - break; - case GL_INT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(int); - break; - case GL_UNSIGNED_INT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned); - break; - case GL_SHORT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(short); - break; - case GL_UNSIGNED_SHORT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned short); - break; - case GL_UNSIGNED_BYTE: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned char); - break; - default: fg::TypeError("Plot::Plot", __LINE__, 1, mDataType); - } -} - -plot_impl::~plot_impl() -{ - CheckGL("Begin Plot::~Plot"); - for (auto it = mVAOMap.begin(); it!=mVAOMap.end(); ++it) { - GLuint vao = it->second; - glDeleteVertexArrays(1, &vao); - } - glDeleteBuffers(1, &mMainVBO); - glDeleteProgram(mMarkerProgram); - CheckGL("End Plot::~Plot"); -} - -void plot_impl::setColor(fg::Color col) -{ - mLineColor[0] = (((int) col >> 24 ) & 0xFF ) / 255.f; - mLineColor[1] = (((int) col >> 16 ) & 0xFF ) / 255.f; - mLineColor[2] = (((int) col >> 8 ) & 0xFF ) / 255.f; - mLineColor[3] = (((int) col ) & 0xFF ) / 255.f; -} - -void plot_impl::setColor(float r, float g, float b) -{ - mLineColor[0] = clampTo01(r); - mLineColor[1] = clampTo01(g); - mLineColor[2] = clampTo01(b); - mLineColor[3] = 1.0f; -} - -GLuint plot_impl::vbo() const -{ - return mMainVBO; -} - -size_t plot_impl::size() const -{ - return mMainVBOsize; -} - -void plot_impl::render(int pWindowId, int pX, int pY, int pVPW, int pVPH) -{ - float range_x = xmax() - xmin(); - float range_y = ymax() - ymin(); - // set scale to zero if input is constant array - // otherwise compute scale factor by standard equation - float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(xmax() - xmin()); - float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(ymax() - ymin()); - - CheckGL("Begin Plot::render"); - float viewWidth = pVPW - (mLeftMargin + mRightMargin + mTickSize/2 ); - float viewHeight = pVPH - (mBottomMargin + mTopMargin + mTickSize ); - float view_scale_x = viewWidth/pVPW; - float view_scale_y = viewHeight/pVPH; - float view_offset_x = (2.0f * (mLeftMargin + mTickSize/2 )/ pVPW ) ; - float view_offset_y = (2.0f * (mBottomMargin + mTickSize )/ pVPH ) ; - /* Enable scissor test to discard anything drawn beyond viewport. - * Set scissor rectangle to clip fragments outside of viewport */ - glScissor(pX + mLeftMargin + mTickSize/2, pY+mBottomMargin + mTickSize/2, - pVPW - mLeftMargin - mRightMargin - mTickSize/2, - pVPH - mBottomMargin - mTopMargin - mTickSize/2); - glEnable(GL_SCISSOR_TEST); - - float coor_offset_x = ( -xmin() * graph_scale_x * view_scale_x); - float coor_offset_y = ( -ymin() * graph_scale_y * view_scale_y); - glm::mat4 transform = glm::translate(glm::mat4(1.f), - glm::vec3(-1 + view_offset_x + coor_offset_x , -1 + view_offset_y + coor_offset_y, 0)); - transform = glm::scale(transform, - glm::vec3(graph_scale_x * view_scale_x , graph_scale_y * view_scale_y ,1)); - - if(mPlotType == fg::FG_LINE) { - glUseProgram(mBorderProgram); - glUniformMatrix4fv(mBorderUniformMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); - glUniform4fv(mBorderUniformColorIndex, 1, mLineColor); - plot_impl::bindResources(pWindowId); - glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); - plot_impl::unbindResources(); - glUseProgram(0); - } - - if(mMarkerType != fg::FG_NONE){ - glEnable(GL_PROGRAM_POINT_SIZE); - glUseProgram(mMarkerProgram); - - glUniformMatrix4fv(mSpriteTMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); - glUniform4fv(mMarkerColIndex, 1, mLineColor); - glUniform1i(mMarkerTypeIndex, mMarkerType); - - plot_impl::bindResources(pWindowId); - glDrawArrays(GL_POINTS, 0, mNumPoints); - plot_impl::unbindResources(); - glUseProgram(0); - glDisable(GL_PROGRAM_POINT_SIZE); - } - - /* Stop clipping and reset viewport to window dimensions */ - glDisable(GL_SCISSOR_TEST); - /* render graph border and axes */ - renderChart(pWindowId, pX, pY, pVPW, pVPH); - - CheckGL("End Plot::render"); -} - -} - namespace fg { -Plot::Plot(unsigned pNumPoints, fg::dtype pDataType, - fg::PlotType pPlotType, fg::MarkerType pMarkerType) +Plot::Plot(const uint pNumPoints, const dtype pDataType, const ChartType pChartType, + const PlotType pPlotType, const MarkerType pMarkerType) { - value = new internal::_Plot(pNumPoints, pDataType, pPlotType, pMarkerType); + mValue = new internal::_Plot(pNumPoints, pDataType, pPlotType, pMarkerType, pChartType); } -Plot::Plot(const Plot& other) +Plot::Plot(const Plot& pOther) { - value = new internal::_Plot(*other.get()); + mValue = new internal::_Plot(*pOther.get()); } Plot::~Plot() { - delete value; -} - -void Plot::setColor(fg::Color col) -{ - value->setColor(col); + delete mValue; } -void Plot::setColor(float r, float g, float b) +void Plot::setColor(const Color pColor) { - value->setColor(r, g, b); + float r = (((int) pColor >> 24 ) & 0xFF ) / 255.f; + float g = (((int) pColor >> 16 ) & 0xFF ) / 255.f; + float b = (((int) pColor >> 8 ) & 0xFF ) / 255.f; + float a = (((int) pColor ) & 0xFF ) / 255.f; + mValue->setColor(r, g, b, a); } -void Plot::setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin) +void Plot::setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { - value->setAxesLimits(pXmax, pXmin, pYmax, pYmin); + mValue->setColor(pRed, pGreen, pBlue, pAlpha); } -void Plot::setAxesTitles(const char* pXTitle, const char* pYTitle) +void Plot::setLegend(const std::string& pLegend) { - value->setAxesTitles(pXTitle, pYTitle); + mValue->setLegend(pLegend); } -float Plot::xmax() const +uint Plot::vertices() const { - return value->xmax(); + return mValue->vbo(); } -float Plot::xmin() const +uint Plot::colors() const { - return value->xmin(); + return mValue->cbo(); } -float Plot::ymax() const +uint Plot::alphas() const { - return value->ymax(); + return mValue->abo(); } -float Plot::ymin() const +uint Plot::verticesSize() const { - return value->ymin(); + return (uint)mValue->vboSize(); } -unsigned Plot::vbo() const +uint Plot::colorsSize() const { - return value->vbo(); + return (uint)mValue->cboSize(); } -unsigned Plot::size() const +uint Plot::alphasSize() const { - return (unsigned)value->size(); + return (uint)mValue->aboSize(); } internal::_Plot* Plot::get() const { - return value; + return mValue; } } diff --git a/src/plot.hpp b/src/plot.hpp index 0ca4e319..fc3bf569 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -9,103 +9,354 @@ #pragma once +#include #include -#include +#include +#include +#include +#include +#include + +#include +#include +#include + #include #include -#include namespace internal { -class plot_impl : public Chart2D { +template +class plot_impl : public AbstractRenderable { protected: /* plot points characteristics */ GLuint mNumPoints; fg::dtype mDataType; GLenum mGLType; - float mLineColor[4]; + bool mIsPVCOn; fg::MarkerType mMarkerType; fg::PlotType mPlotType; /* OpenGL Objects */ - GLuint mMainVBO; - size_t mMainVBOsize; + GLuint mPlotProgram; GLuint mMarkerProgram; - /* shared variable index locations */ - GLuint mPointIndex; - GLuint mMarkerColIndex; + /* shaderd variable index locations */ + GLuint mPlotMatIndex; + GLuint mPlotPVCOnIndex; + GLuint mPlotUColorIndex; + GLuint mPlotRangeIndex; + GLuint mPlotPointIndex; + GLuint mPlotColorIndex; + GLuint mPlotAlphaIndex; + + GLuint mMarkerPVCOnIndex; GLuint mMarkerTypeIndex; - GLuint mSpriteTMatIndex; + GLuint mMarkerColIndex; + GLuint mMarkerMatIndex; + GLuint mMarkerPointIndex; + GLuint mMarkerColorIndex; + GLuint mMarkerAlphaIndex; + + float mRange[6]; std::map mVAOMap; /* bind and unbind helper functions * for rendering resources */ - void bindResources(int pWindowId); - void unbindResources() const; + void bindResources(const int pWindowId) + { + if (mVAOMap.find(pWindowId) == mVAOMap.end()) { + GLuint vao = 0; + /* create a vertex array object + * with appropriate bindings */ + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + // attach vertices + glEnableVertexAttribArray(mPlotPointIndex); + glBindBuffer(GL_ARRAY_BUFFER, mVBO); + glVertexAttribPointer(mPlotPointIndex, 2, mGLType, GL_FALSE, 0, 0); + // attach colors + glEnableVertexAttribArray(mPlotColorIndex); + glBindBuffer(GL_ARRAY_BUFFER, mCBO); + glVertexAttribPointer(mPlotColorIndex, 3, GL_FLOAT, GL_FALSE, 0, 0); + // attach alphas + glEnableVertexAttribArray(mPlotAlphaIndex); + glBindBuffer(GL_ARRAY_BUFFER, mABO); + glVertexAttribPointer(mPlotAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); + glBindVertexArray(0); + /* store the vertex array object corresponding to + * the window instance in the map */ + mVAOMap[pWindowId] = vao; + } + + glBindVertexArray(mVAOMap[pWindowId]); + } + + void unbindResources() const + { + glBindVertexArray(0); + } + + void computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput, + const int pX, const int pY, + const int pVPW, const int pVPH) + { + // identity matrix + static const glm::mat4 I(1.0f); + + float range_x = mRange[1] - mRange[0]; + float range_y = mRange[3] - mRange[2]; + // set scale to zero if input is constant array + // otherwise compute scale factor by standard equation + float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); + float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); + + float coor_offset_x = ( -mRange[0] * graph_scale_x); + float coor_offset_y = ( -mRange[2] * graph_scale_y); + + if (PLOT_TYPE == fg::FG_3D) { + static const glm::mat4 VIEW = glm::lookAt(glm::vec3(-1.f,0.5f, 1.f), + glm::vec3( 1.f,-1.f,-1.f), + glm::vec3( 0.f, 1.f, 0.f)); + static const glm::mat4 PROJECTION = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 100.f); + + float range_z = mRange[5] - mRange[4]; + float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(range_z); + float coor_offset_z = (-mRange[4] * graph_scale_z); + + glm::mat4 rMat = glm::rotate(I, + -glm::radians(90.f), glm::vec3(1,0,0)); + glm::mat4 tMat = glm::translate(I, + glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)); + glm::mat4 sMat = glm::scale(I, + glm::vec3(1.0f * graph_scale_x, -1.0f * graph_scale_y, 1.0f * graph_scale_z)); + + glm::mat4 model = rMat * tMat * sMat; + + pOut = PROJECTION * VIEW * model; + } else if (PLOT_TYPE == fg::FG_2D) { + //FIXME: Using hard constants for now, find a way to get chart values + const float lMargin = 68; + const float rMargin = 8; + const float tMargin = 8; + const float bMargin = 32; + const float tickSize = 10; + + float viewWidth = pVPW - (lMargin + rMargin + tickSize/2); + float viewHeight = pVPH - (bMargin + tMargin + tickSize ); + float view_scale_x = viewWidth/pVPW; + float view_scale_y = viewHeight/pVPH; + + coor_offset_x *= view_scale_x; + coor_offset_y *= view_scale_y; + + float view_offset_x = (2.0f * (lMargin + tickSize/2 )/ pVPW ) ; + float view_offset_y = (2.0f * (bMargin + tickSize )/ pVPH ) ; + + glm::mat4 tMat = glm::translate(I, + glm::vec3(-1 + view_offset_x + coor_offset_x , -1 + view_offset_y + coor_offset_y, 0)); + pOut = glm::scale(tMat, + glm::vec3(graph_scale_x * view_scale_x , graph_scale_y * view_scale_y ,1)); + + glScissor(pX + lMargin + tickSize/2, pY+bMargin + tickSize/2, + pVPW - lMargin - rMargin - tickSize/2, + pVPH - bMargin - tMargin - tickSize/2); + } + } public: - plot_impl(unsigned pNumPoints, fg::dtype pDataType, fg::PlotType, fg::MarkerType); - ~plot_impl(); + plot_impl(const uint pNumPoints, const fg::dtype pDataType, + const fg::PlotType pPlotType, const fg::MarkerType pMarkerType) + : mNumPoints(pNumPoints), mDataType(pDataType), mGLType(dtype2gl(mDataType)), + mIsPVCOn(0), mMarkerType(pMarkerType), mPlotType(pPlotType), + mPlotProgram(-1), mMarkerProgram(-1), mPlotMatIndex(-1), mPlotPVCOnIndex(-1), + mPlotUColorIndex(-1), mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), + mPlotAlphaIndex(-1), mMarkerPVCOnIndex(-1), mMarkerTypeIndex(-1), + mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), + mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) + { + mColor[0] = 0.0f; + mColor[1] = 1.0f; + mColor[2] = 0.0f; + mColor[3] = 1.0f; + mLegend = std::string(""); + + CheckGL("Begin plot_impl::plot_impl"); + + if (PLOT_TYPE==fg::FG_2D) { + mPlotProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::histogram_fs.c_str()); + mMarkerProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::marker_fs.c_str()); + } else if (PLOT_TYPE==fg::FG_3D) { + mPlotProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); + mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); + } + + mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); + mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); + mPlotUColorIndex = glGetUniformLocation(mPlotProgram, "barColor"); + mPlotRangeIndex = glGetUniformLocation(mPlotProgram, "minmaxs"); + mPlotPointIndex = glGetUniformLocation(mPlotProgram, "point"); + mPlotColorIndex = glGetUniformLocation(mPlotProgram, "color"); + mPlotAlphaIndex = glGetUniformLocation(mPlotProgram, "alpha"); + + mMarkerPVCOnIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); + mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); + mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); + mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); + mMarkerPointIndex = glGetUniformLocation(mMarkerProgram, "point"); + mMarkerColorIndex = glGetUniformLocation(mMarkerProgram, "color"); + mMarkerAlphaIndex = glGetUniformLocation(mMarkerProgram, "alpha"); + + if (PLOT_TYPE==fg::FG_2D) { + mVBOSize = 2*mNumPoints; + } else if (PLOT_TYPE==fg::FG_3D) { + mVBOSize = 3*mNumPoints; + } + mCBOSize = 3*mNumPoints; + mABOSize = mNumPoints; + +#define PLOT_CREATE_BUFFERS(type) \ + mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ + mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ + mABO = createBuffer(GL_ARRAY_BUFFER, mABOSize, NULL, GL_DYNAMIC_DRAW); \ + mVBOSize *= sizeof(type); \ + mCBOSize *= sizeof(float); \ + mABOSize *= sizeof(float); + + switch(mGLType) { + case GL_FLOAT : PLOT_CREATE_BUFFERS(float) ; break; + case GL_INT : PLOT_CREATE_BUFFERS(int) ; break; + case GL_UNSIGNED_INT : PLOT_CREATE_BUFFERS(uint) ; break; + case GL_SHORT : PLOT_CREATE_BUFFERS(short) ; break; + case GL_UNSIGNED_SHORT : PLOT_CREATE_BUFFERS(ushort); break; + case GL_UNSIGNED_BYTE : PLOT_CREATE_BUFFERS(float) ; break; + default: fg::TypeError("plot_impl::plot_impl", __LINE__, 1, mDataType); + } +#undef PLOT_CREATE_BUFFERS + CheckGL("End plot_impl::plot_impl"); + } + + ~plot_impl() + { + CheckGL("Begin plot_impl::~plot_impl"); + for (auto it = mVAOMap.begin(); it!=mVAOMap.end(); ++it) { + GLuint vao = it->second; + glDeleteVertexArrays(1, &vao); + } + glDeleteBuffers(1, &mVBO); + glDeleteBuffers(1, &mCBO); + glDeleteBuffers(1, &mABO); + glDeleteProgram(mPlotProgram); + glDeleteProgram(mMarkerProgram); + CheckGL("End plot_impl::~plot_impl"); + } - void setColor(fg::Color col); - void setColor(float r, float g, float b); - GLuint vbo() const; - size_t size() const; + void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) + { + CheckGL("Begin plot_impl::render"); + glScissor(pX, pY, pVPW, pVPH); + glEnable(GL_SCISSOR_TEST); - void render(int pWindowId, int pX, int pY, int pViewPortWidth, int pViewPortHeight); + glm::mat4 mvp(1.0); + computeTransformMat(mvp, pTransform, pX, pY, pVPW, pVPH); + + if (mPlotType == fg::FG_LINE) { + glUseProgram(mPlotProgram); + + if (PLOT_TYPE== fg::FG_3D) { + glUniform2fv(mPlotRangeIndex, 3, mRange); + } + glUniformMatrix4fv(mPlotMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); + glUniform1i(mPlotPVCOnIndex, mIsPVCOn); + glUniform4fv(mPlotUColorIndex, 1, mColor); + + plot_impl::bindResources(pWindowId); + glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); + plot_impl::unbindResources(); + glUseProgram(0); + } + + if (mMarkerType != fg::FG_NONE) { + glEnable(GL_PROGRAM_POINT_SIZE); + glUseProgram(mMarkerProgram); + + glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); + glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); + glUniform1i(mMarkerTypeIndex, mMarkerType); + glUniform4fv(mMarkerColIndex, 1, mColor); + + plot_impl::bindResources(pWindowId); + glDrawArrays(GL_POINTS, 0, mNumPoints); + plot_impl::unbindResources(); + glUseProgram(0); + glDisable(GL_PROGRAM_POINT_SIZE); + } + + glDisable(GL_SCISSOR_TEST); + CheckGL("End plot_impl::render"); + } }; class _Plot { private: - std::shared_ptr plt; + std::shared_ptr mPlot; public: - _Plot(unsigned pNumPoints, fg::dtype pDataType, fg::PlotType pType, fg::MarkerType mType) - : plt(std::make_shared(pNumPoints, pDataType, pType, mType)) {} - - inline const std::shared_ptr& impl() const { - return plt; + _Plot(const uint pNumPoints, const fg::dtype pDataType, + const fg::PlotType pPlotType, const fg::MarkerType pMarkerType, + const fg::ChartType pChartType) { + if (pChartType == fg::FG_2D) { + mPlot = std::make_shared< plot_impl >(pNumPoints, pDataType, + pPlotType, pMarkerType); + } else { + mPlot = std::make_shared< plot_impl >(pNumPoints, pDataType, + pPlotType, pMarkerType); + } } - inline void setColor(fg::Color col) { - plt->setColor(col); + inline const std::shared_ptr& impl() const { + return mPlot; } - inline void setColor(float r, float g, float b) { - plt->setColor(r, g, b); + inline void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { + mPlot->setColor(pRed, pGreen, pBlue, pAlpha); } - inline void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin) { - plt->setAxesLimits(pXmax, pXmin, pYmax, pYmin); + inline void setLegend(const std::string pLegend) { + mPlot->setLegend(pLegend); } - inline void setAxesTitles(const char* pXTitle, const char* pYTitle) { - plt->setAxesTitles(pXTitle, pYTitle); + inline GLuint vbo() const { + return mPlot->vbo(); } - inline float xmax() const { - return plt->xmax(); + inline GLuint cbo() const { + return mPlot->cbo(); } - inline float xmin() const { - return plt->xmin(); + inline GLuint abo() const { + return mPlot->abo(); } - inline float ymax() const { - return plt->ymax(); + inline size_t vboSize() const { + return mPlot->vboSize(); } - inline float ymin() const { - return plt->ymin(); + inline size_t cboSize() const { + return mPlot->cboSize(); } - inline GLuint vbo() const { - return plt->vbo(); + inline size_t aboSize() const { + return mPlot->aboSize(); } - inline size_t size() const { - return plt->size(); + inline void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) const { + mPlot->render(pWindowId, pX, pY, pVPW, pVPH, pTransform); } }; diff --git a/src/plot3.cpp b/src/plot3.cpp deleted file mode 100644 index 75238840..00000000 --- a/src/plot3.cpp +++ /dev/null @@ -1,354 +0,0 @@ -/******************************************************* - * Copyright (c) 2015-2019, ArrayFire - * All rights reserved. - * - * This file is distributed under 3-clause BSD license. - * The complete license agreement can be obtained at: - * http://arrayfire.com/licenses/BSD-3-Clause - ********************************************************/ - -#include -#include -#include - -#include - -#include -#include -#include - -using namespace std; - -static const char *gMarkerVertexShaderSrc = -"#version 330\n" -"in vec3 point;\n" -"uniform vec2 minmaxs[3];\n" -"out vec4 hpoint;\n" -"uniform mat4 transform;\n" -"void main(void) {\n" -" gl_Position = transform * vec4(point.xyz, 1);\n" -" hpoint=vec4(point.xyz,1);\n" -" gl_PointSize=10;\n" -"}"; - -const char *gPlot3FragmentShaderSrc = -"#version 330\n" -"uniform vec2 minmaxs[3];\n" -"in vec4 hpoint;\n" -"out vec4 outputColor;\n" -"vec3 hsv2rgb(vec3 c){\n" -" vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n" -" vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n" -" return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n" -"}\n" -"void main(void) {\n" -" bool nin_bounds = (hpoint.x > minmaxs[0].x || hpoint.x < minmaxs[0].y ||\n" -" hpoint.y > minmaxs[1].x || hpoint.y < minmaxs[1].y || hpoint.z < minmaxs[2].y);\n" -" float height = (minmaxs[2].x- hpoint.z)/(minmaxs[2].x-minmaxs[2].y);\n" -" if(nin_bounds) discard;\n" -" outputColor = vec4(hsv2rgb(vec3(height, 1.f, 1.f)),1);\n" -"}"; - -static const char *gMarkerSpriteFragmentShaderSrc = -"#version 330\n" -"uniform int marker_type;\n" -"uniform vec4 line_color;\n" -"in vec4 hpoint;\n" -"out vec4 outputColor;\n" -"void main(void) {\n" -" vec4 unused = hpoint;\n" -" float dist = sqrt( (gl_PointCoord.x - 0.5) * (gl_PointCoord.x-0.5) + (gl_PointCoord.y-0.5) * (gl_PointCoord.y-0.5) );\n" -" bool in_bounds;\n" -" switch(marker_type) {\n" -" case 1:\n" -" in_bounds = dist < 0.3;\n" -" break;\n" -" case 2:\n" -" in_bounds = ( (dist > 0.3) && (dist<0.5) );\n" -" break;\n" -" case 3:\n" -" in_bounds = ((gl_PointCoord.x < 0.15) || (gl_PointCoord.x > 0.85)) ||\n" -" ((gl_PointCoord.y < 0.15) || (gl_PointCoord.y > 0.85));\n" -" break;\n" -" case 4:\n" -" in_bounds = (2*(gl_PointCoord.x - 0.25) - (gl_PointCoord.y + 0.5) < 0) && (2*(gl_PointCoord.x - 0.25) + (gl_PointCoord.y + 0.5) > 1);\n" -" break;\n" -" case 5:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.13 ||\n" -" abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.13 ;\n" -" break;\n" -" case 6:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5)) < 0.07 ||\n" -" abs((gl_PointCoord.y - 0.5)) < 0.07;\n" -" break;\n" -" case 7:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.07 ||\n" -" abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.07 ||\n" -" abs((gl_PointCoord.x - 0.5)) < 0.07 ||\n" -" abs((gl_PointCoord.y - 0.5)) < 0.07;\n" -" break;\n" -" case 8:\n" -" in_bounds = true;\n" -" break;\n" -" default:\n" -" in_bounds = true;\n" -" }\n" -" if(!in_bounds)\n" -" discard;\n" -" else\n" -" outputColor = line_color;\n" -"}"; - - -namespace internal -{ - -void plot3_impl::bindResources(int pWindowId) -{ - if (mVAOMap.find(pWindowId) == mVAOMap.end()) { - GLuint vao = 0; - /* create a vertex array object - * with appropriate bindings */ - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - // attach plot vertices - glEnableVertexAttribArray(mPointIndex); - glBindBuffer(GL_ARRAY_BUFFER, mMainVBO); - glVertexAttribPointer(mPointIndex, 3, mDataType, GL_FALSE, 0, 0); - //attach indices - glBindVertexArray(0); - /* store the vertex array object corresponding to - * the window instance in the map */ - mVAOMap[pWindowId] = vao; - } - - glBindVertexArray(mVAOMap[pWindowId]); -} - -void plot3_impl::unbindResources() const { glBindVertexArray(0); } - -plot3_impl::plot3_impl(unsigned pNumPoints, fg::dtype pDataType, fg::PlotType pPlotType, fg::MarkerType pMarkerType) - : Chart3D(), mNumPoints(pNumPoints), - mDataType(gl_dtype(pDataType)), mPlotType(pPlotType), - mMainVBO(0), mMainVBOsize(0), - mIndexVBOsize(0), mPointIndex(0), mMarkerTypeIndex(0), - mMarkerColIndex(0), mSpriteTMatIndex(0), mPlot3PointIndex(0), - mPlot3TMatIndex(0), mPlot3RangeIndex(0) -{ - CheckGL("Begin plot3_impl::plot3_impl"); - mPointIndex = mBorderAttribPointIndex; - mMarkerType = pMarkerType; - mPlot3Program = initShaders(gMarkerVertexShaderSrc, gPlot3FragmentShaderSrc); - mMarkerProgram = initShaders(gMarkerVertexShaderSrc, gMarkerSpriteFragmentShaderSrc); - - mPlot3PointIndex = glGetAttribLocation (mPlot3Program, "point"); - mPlot3TMatIndex = glGetUniformLocation(mPlot3Program, "transform"); - mPlot3RangeIndex = glGetUniformLocation(mPlot3Program, "minmaxs"); - - mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); - mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "line_color"); - mSpriteTMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); - - unsigned total_points = 3 * mNumPoints; - - // buffersubdata calls on mMainVBO - // will only update the points data - switch(mDataType) { - case GL_FLOAT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(float); - break; - case GL_INT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(int); - break; - case GL_UNSIGNED_INT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned); - break; - case GL_UNSIGNED_BYTE: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned char); - break; - default: fg::TypeError("Plot::Plot", __LINE__, 1, pDataType); - } - CheckGL("End plot3_impl::plot3_impl"); -} - -plot3_impl::~plot3_impl() -{ - CheckGL("Begin Plot::~Plot"); - for (auto it = mVAOMap.begin(); it!=mVAOMap.end(); ++it) { - GLuint vao = it->second; - glDeleteVertexArrays(1, &vao); - } - glDeleteBuffers(1, &mMainVBO); - glDeleteProgram(mMarkerProgram); - glDeleteProgram(mPlot3Program); - CheckGL("End Plot::~Plot"); -} - -void plot3_impl::setColor(fg::Color col) -{ - mLineColor[0] = (((int) col >> 24 ) & 0xFF ) / 255.f; - mLineColor[1] = (((int) col >> 16 ) & 0xFF ) / 255.f; - mLineColor[2] = (((int) col >> 8 ) & 0xFF ) / 255.f; - mLineColor[3] = (((int) col ) & 0xFF ) / 255.f; -} - -void plot3_impl::setColor(float r, float g, float b) -{ - mLineColor[0] = clampTo01(r); - mLineColor[1] = clampTo01(g); - mLineColor[2] = clampTo01(b); - mLineColor[3] = 1.0f; -} - -GLuint plot3_impl::vbo() const { return mMainVBO; } - -size_t plot3_impl::size() const { return mMainVBOsize; } - -void plot3_impl::render(int pWindowId, int pX, int pY, int pVPW, int pVPH) -{ - float range_x = xmax() - xmin(); - float range_y = ymax() - ymin(); - float range_z = zmax() - zmin(); - // set scale to zero if input is constant array - // otherwise compute scale factor by standard equation - float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(xmax() - xmin()); - float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(ymax() - ymin()); - float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(zmax() - zmin()); - - CheckGL("Begin plot3_impl::render"); - - float coor_offset_x = ( -xmin() * graph_scale_x); - float coor_offset_y = ( -ymin() * graph_scale_y); - float coor_offset_z = ( -zmin() * graph_scale_z); - - glm::mat4 model = glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(1,0,0)) * glm::translate(glm::mat4(1.f), glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)) * glm::scale(glm::mat4(1.f), glm::vec3(1.0f * graph_scale_x, -1.0f * graph_scale_y, 1.0f * graph_scale_z)); - glm::mat4 view = glm::lookAt(glm::vec3(-1,0.5f,1.0f), glm::vec3(1,-1,-1),glm::vec3(0,1,0)); - glm::mat4 projection = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 100.f); - glm::mat4 mvp = projection * view * model; - glm::mat4 transform = mvp; - - if(mPlotType != fg::FG_SCATTER) { - glUseProgram(mPlot3Program); - GLfloat range[] = {xmax(), xmin(), ymax(), ymin(), zmax(), zmin()}; - - glUniform2fv(mPlot3RangeIndex, 3, range); - glUniformMatrix4fv(mPlot3TMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); - - bindResources(pWindowId); - glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); - unbindResources(); - glUseProgram(0); - } - - if(mMarkerType != fg::FG_NONE) { - glEnable(GL_PROGRAM_POINT_SIZE); - glUseProgram(mMarkerProgram); - - glUniformMatrix4fv(mSpriteTMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); - glUniform4fv(mMarkerColIndex, 1, WHITE); - glUniform1i(mMarkerTypeIndex, mMarkerType); - - bindResources(pWindowId); - glDrawArrays(GL_POINTS, 0, mNumPoints); - unbindResources(); - glUseProgram(0); - glDisable(GL_PROGRAM_POINT_SIZE); - } - - /* render graph border and axes */ - renderChart(pWindowId, pX, pY, pVPW, pVPH); - - CheckGL("End plot3_impl::render"); -} - -} - - -namespace fg -{ - -Plot3::Plot3(unsigned pNumPoints, dtype pDataType, PlotType pPlotType, MarkerType pMarkerType) -{ - value = new internal::_Plot3(pNumPoints, pDataType, pPlotType, pMarkerType); -} - -Plot3::Plot3(const Plot3& other) -{ - value = new internal::_Plot3(*other.get()); -} - -Plot3::~Plot3() -{ - delete value; -} - -void Plot3::setColor(fg::Color col) -{ - value->setColor(col); -} - -void Plot3::setColor(float r, float g, float b) -{ - value->setColor(r, g, b); -} - -void Plot3::setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, float pZmax, float pZmin) -{ - value->setAxesLimits(pXmax, pXmin, pYmax, pYmin, pZmax, pZmin); -} - -void Plot3::setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle) -{ - value->setAxesTitles(pXTitle, pYTitle, pZTitle); -} - -float Plot3::xmax() const -{ - return value->xmax(); -} - -float Plot3::xmin() const -{ - return value->xmin(); -} - -float Plot3::ymax() const -{ - return value->ymax(); -} - -float Plot3::ymin() const -{ - return value->ymin(); -} - -float Plot3::zmax() const -{ - return value->zmax(); -} - -float Plot3::zmin() const -{ - return value->zmin(); -} - -unsigned Plot3::vbo() const -{ - return value->vbo(); -} - -unsigned Plot3::size() const -{ - return (unsigned)value->size(); -} - -internal::_Plot3* Plot3::get() const -{ - return value; -} - -} diff --git a/src/plot3.hpp b/src/plot3.hpp deleted file mode 100644 index 9da7528e..00000000 --- a/src/plot3.hpp +++ /dev/null @@ -1,126 +0,0 @@ -/******************************************************* -* Copyright (c) 2015-2019, ArrayFire -* All rights reserved. -* -* This file is distributed under 3-clause BSD license. -* The complete license agreement can be obtained at: -* http://arrayfire.com/licenses/BSD-3-Clause -********************************************************/ - -#pragma once - -#include -#include -#include -#include -#include - -namespace internal -{ - -class plot3_impl : public Chart3D { - protected: - /* plot points characteristics */ - GLuint mNumPoints; - GLenum mDataType; - float mLineColor[4]; - fg::MarkerType mMarkerType; - fg::PlotType mPlotType; - /* OpenGL Objects */ - GLuint mMainVBO; - size_t mMainVBOsize; - size_t mIndexVBOsize; - GLuint mMarkerProgram; - GLuint mPlot3Program; - /* shared variable index locations */ - GLuint mPointIndex; - GLuint mMarkerTypeIndex; - GLuint mMarkerColIndex; - GLuint mSpriteTMatIndex; - GLuint mPlot3PointIndex; - GLuint mPlot3TMatIndex; - GLuint mPlot3RangeIndex; - - std::map mVAOMap; - - /* bind and unbind helper functions - * for rendering resources */ - void bindResources(int pWindowId); - void unbindResources() const; - - public: - plot3_impl(unsigned pNumPoints, fg::dtype pDataType, fg::PlotType pPlotType, fg::MarkerType pMarkerType); - ~plot3_impl(); - - void setColor(fg::Color col); - void setColor(float r, float g, float b); - GLuint vbo() const; - size_t size() const; - - void render(int pWindowId, int pX, int pY, int pViewPortWidth, int pViewPortHeight); -}; - -class _Plot3 { - private: - std::shared_ptr plt; - - public: - _Plot3(unsigned pNumPoints, fg::dtype pDataType, fg::PlotType pPlotType=fg::FG_LINE, fg::MarkerType pMarkerType=fg::FG_NONE) { - plt = std::make_shared(pNumPoints, pDataType, pPlotType, pMarkerType); - } - - inline const std::shared_ptr& impl() const { - return plt; - } - - inline void setColor(fg::Color col) { - plt->setColor(col); - } - - inline void setColor(float r, float g, float b) { - plt->setColor(r, g, b); - } - - inline void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, float pZmax, float pZmin) { - plt->setAxesLimits(pXmax, pXmin, pYmax, pYmin, pZmax, pZmin); - } - - inline void setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle) - { - plt->setAxesTitles(pXTitle, pYTitle, pZTitle); - } - - inline float xmax() const { - return plt->xmax(); - } - - inline float xmin() const { - return plt->xmin(); - } - - inline float ymax() const { - return plt->ymax(); - } - - inline float ymin() const { - return plt->ymin(); - } - - inline float zmax() const { - return plt->zmax(); - } - - inline float zmin() const { - return plt->zmin(); - } - - inline GLuint vbo() const { - return plt->vbo(); - } - - inline size_t size() const { - return plt->size(); - } -}; - -} diff --git a/src/shaders/chart_fs.glsl b/src/shaders/chart_fs.glsl new file mode 100644 index 00000000..84721949 --- /dev/null +++ b/src/shaders/chart_fs.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform vec4 color; + +out vec4 outputColor; + +void main(void) +{ + outputColor = color; +} diff --git a/src/shaders/chart_vs.glsl b/src/shaders/chart_vs.glsl new file mode 100644 index 00000000..94130222 --- /dev/null +++ b/src/shaders/chart_vs.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform mat4 transform; + +in vec3 point; + +void main(void) +{ + gl_Position = transform * vec4(point.xyz, 1); +} diff --git a/src/shaders/font_fs.glsl b/src/shaders/font_fs.glsl new file mode 100644 index 00000000..a0a33b53 --- /dev/null +++ b/src/shaders/font_fs.glsl @@ -0,0 +1,14 @@ +#version 330 + +uniform sampler2D tex; +uniform vec4 textColor; + +in vec2 texCoord; +out vec4 outputColor; + +void main() +{ + vec4 texC = texture(tex, texCoord); + vec4 alpha = vec4(1.0, 1.0, 1.0, texC.r); + outputColor = alpha*textColor; +} diff --git a/src/shaders/font_vs.glsl b/src/shaders/font_vs.glsl new file mode 100644 index 00000000..ac3f2a0c --- /dev/null +++ b/src/shaders/font_vs.glsl @@ -0,0 +1,15 @@ +#version 330 + +uniform mat4 projectionMatrix; +uniform mat4 modelViewMatrix; + +layout (location = 0) in vec2 inPosition; +layout (location = 1) in vec2 inCoord; + +out vec2 texCoord; + +void main() +{ + gl_Position = projectionMatrix*modelViewMatrix*vec4(inPosition, 0.0, 1.0); + texCoord = inCoord; +} diff --git a/src/shaders/histogram_fs.glsl b/src/shaders/histogram_fs.glsl new file mode 100644 index 00000000..01192174 --- /dev/null +++ b/src/shaders/histogram_fs.glsl @@ -0,0 +1,12 @@ +#version 330 + +uniform bool isPVCOn; +uniform vec4 barColor; + +in vec4 pervcol; +out vec4 outColor; + +void main(void) +{ + outColor = isPVCOn ? pervcol : barColor; +} diff --git a/src/shaders/histogram_vs.glsl b/src/shaders/histogram_vs.glsl new file mode 100644 index 00000000..d29d2688 --- /dev/null +++ b/src/shaders/histogram_vs.glsl @@ -0,0 +1,29 @@ +#version 330 + +uniform float ymax; +uniform float nbins; +uniform mat4 transform; + +in vec2 point; +in float freq; +in vec3 color; +in float alpha; + +out vec4 pervcol; + +void main(void) +{ + float binId = gl_InstanceID; + float deltax = 2.0/nbins; + float deltay = 2.0/ymax; + float xcurr = -1.0 + binId * deltax; + if (point.x==1) { + xcurr += deltax; + } + float ycurr = -1.0; + if (point.y==1) { + ycurr += deltay * freq; + } + pervcol = vec4(color, alpha); + gl_Position = transform * vec4(xcurr, ycurr, 0, 1); +} diff --git a/src/shaders/image_fs.glsl b/src/shaders/image_fs.glsl new file mode 100644 index 00000000..a0338bea --- /dev/null +++ b/src/shaders/image_fs.glsl @@ -0,0 +1,31 @@ +#version 330 + +const int size = 259; + +layout(std140) uniform ColorMap +{ + vec4 ch[size]; +}; + +uniform float cmaplen; +uniform sampler2D tex; +uniform bool isGrayScale; + +in vec2 texcoord; +out vec4 fragColor; + +void main() +{ + vec4 tcolor = texture(tex, texcoord); + vec4 clrs = vec4(1, 0, 0, 1); + if(isGrayScale) + clrs = vec4(tcolor.r, tcolor.r, tcolor.r, 1); + else + clrs = tcolor; + vec4 fidx = (cmaplen-1) * clrs; + ivec4 idx = ivec4(fidx.x, fidx.y, fidx.z, fidx.w); + float r_ch = ch[idx.x].r; + float g_ch = ch[idx.y].g; + float b_ch = ch[idx.z].b; + fragColor = vec4(r_ch, g_ch , b_ch, 1); +} diff --git a/src/shaders/image_vs.glsl b/src/shaders/image_vs.glsl new file mode 100644 index 00000000..0d7dfe56 --- /dev/null +++ b/src/shaders/image_vs.glsl @@ -0,0 +1,14 @@ +#version 330 + +layout(location = 0) in vec2 pos; +layout(location = 1) in vec2 tex; + +uniform mat4 matrix; + +out vec2 texcoord; + +void main() +{ + texcoord = tex; + gl_Position = matrix * vec4(pos, 0.0, 1.0); +} diff --git a/src/shaders/marker2d_vs.glsl b/src/shaders/marker2d_vs.glsl new file mode 100644 index 00000000..3d42fb43 --- /dev/null +++ b/src/shaders/marker2d_vs.glsl @@ -0,0 +1,16 @@ +#version 330 + +uniform mat4 transform; + +in vec2 point; +in vec3 color; +in float alpha; + +out vec4 pervcol; + +void main(void) +{ + pervcol = vec4(color, alpha); + gl_Position = transform * vec4(point.xy, 0, 1); + gl_PointSize= 10; +} diff --git a/src/shaders/marker_fs.glsl b/src/shaders/marker_fs.glsl new file mode 100644 index 00000000..d0a95875 --- /dev/null +++ b/src/shaders/marker_fs.glsl @@ -0,0 +1,49 @@ +#version 330 + +uniform bool isPVCOn; +uniform int marker_type; +uniform vec4 marker_color; + +in vec4 pervcol; +out vec4 outColor; + +void main(void) +{ + float dist = sqrt( (gl_PointCoord.x - 0.5) * (gl_PointCoord.x-0.5) + (gl_PointCoord.y-0.5) * (gl_PointCoord.y-0.5) ); + bool in_bounds; + switch(marker_type) { + case 1: + in_bounds = dist < 0.3; + break; + case 2: + in_bounds = ( (dist > 0.3) && (dist<0.5) ); + break; + case 3: + in_bounds = ((gl_PointCoord.x < 0.15) || (gl_PointCoord.x > 0.85)) || + ((gl_PointCoord.y < 0.15) || (gl_PointCoord.y > 0.85)); + break; + case 4: + in_bounds = (2*(gl_PointCoord.x - 0.25) - (gl_PointCoord.y + 0.5) < 0) && (2*(gl_PointCoord.x - 0.25) + (gl_PointCoord.y + 0.5) > 1); + break; + case 5: + in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.13 || + abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.13 ; + break; + case 6: + in_bounds = abs((gl_PointCoord.x - 0.5)) < 0.07 || + abs((gl_PointCoord.y - 0.5)) < 0.07; + break; + case 7: + in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.07 || + abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.07 || + abs((gl_PointCoord.x - 0.5)) < 0.07 || + abs((gl_PointCoord.y - 0.5)) < 0.07; + break; + default: + in_bounds = true; + } + if(!in_bounds) + discard; + else + outColor = isPVCOn ? pervcol : marker_color; +} diff --git a/src/shaders/plot3_fs.glsl b/src/shaders/plot3_fs.glsl new file mode 100644 index 00000000..91848239 --- /dev/null +++ b/src/shaders/plot3_fs.glsl @@ -0,0 +1,29 @@ +#version 330 + +uniform bool isPVCOn; +uniform vec2 minmaxs[3]; + +in vec4 pervcol; +in vec4 hpoint; + +out vec4 outColor; + +vec3 hsv2rgb(vec3 c) +{ + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main(void) +{ + bool nin_bounds = (hpoint.x > minmaxs[0].x || hpoint.x < minmaxs[0].y || + hpoint.y > minmaxs[1].x || hpoint.y < minmaxs[1].y || hpoint.z < minmaxs[2].y); + + float height = (minmaxs[2].x- hpoint.z)/(minmaxs[2].x-minmaxs[2].y); + + if(nin_bounds) + discard; + else + outColor = isPVCOn ? pervcol : vec4(hsv2rgb(vec3(height, 1.f, 1.f)),1); +} diff --git a/src/shaders/plot3_vs.glsl b/src/shaders/plot3_vs.glsl new file mode 100644 index 00000000..f7435107 --- /dev/null +++ b/src/shaders/plot3_vs.glsl @@ -0,0 +1,19 @@ +#version 330 + +uniform mat4 transform; +uniform vec2 minmaxs[3]; + +in vec3 point; +in vec3 color; +in float alpha; + +out vec4 hpoint; +out vec4 pervcol; + +void main(void) +{ + hpoint = vec4(point.xyz,1); + pervcol = vec4(color, alpha); + gl_Position = transform * vec4(point.xyz, 1); + gl_PointSize= 10; +} diff --git a/src/shaders/tick_fs.glsl b/src/shaders/tick_fs.glsl new file mode 100644 index 00000000..967e2aba --- /dev/null +++ b/src/shaders/tick_fs.glsl @@ -0,0 +1,16 @@ +#version 330 + +uniform bool isYAxis; +uniform vec4 tick_color; + +out vec4 outputColor; + +void main(void) +{ + bool y_axis = isYAxis && abs(gl_PointCoord.y)>0.2; + bool x_axis = !isYAxis && abs(gl_PointCoord.x)>0.2; + if(y_axis || x_axis) + discard; + else + outputColor = tick_color; +} diff --git a/src/surface.cpp b/src/surface.cpp index d5d349e7..7885dfd3 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -10,125 +10,20 @@ #include #include #include - -#include +#include +#include +#include #include #include #include -using namespace std; - -static const char *gMarkerVertexShaderSrc = -"#version 330\n" -"in vec3 point;\n" -"uniform vec2 minmaxs[3];\n" -"out vec4 hpoint;\n" -"uniform mat4 transform;\n" -"void main(void) {\n" -" gl_Position = transform * vec4(point.xyz, 1);\n" -" hpoint=vec4(point.xyz,1);\n" -" gl_PointSize=10;\n" -"}"; - -const char *gSurfFragmentShaderSrc = -"#version 330\n" -"uniform vec2 minmaxs[3];\n" -"in vec4 hpoint;\n" -"out vec4 outputColor;\n" -"vec3 hsv2rgb(vec3 c){\n" -" vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n" -" vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n" -" return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n" -"}\n" -"void main(void) {\n" -" bool nin_bounds = (hpoint.x > minmaxs[0].x || hpoint.x < minmaxs[0].y ||\n" -" hpoint.y > minmaxs[1].x || hpoint.y < minmaxs[1].y || hpoint.z < minmaxs[2].y);\n" -" float height = (minmaxs[2].x- hpoint.z)/(minmaxs[2].x-minmaxs[2].y);\n" -" if(nin_bounds) discard;\n" -" outputColor = vec4(hsv2rgb(vec3(height, 1.f, 1.f)),1);\n" -"}"; - -static const char *gMarkerSpriteFragmentShaderSrc = -"#version 330\n" -"uniform int marker_type;\n" -"uniform vec4 line_color;\n" -"in vec4 hpoint;\n" -"out vec4 outputColor;\n" -"void main(void) {\n" -" vec4 unused = hpoint;\n" -" float dist = sqrt( (gl_PointCoord.x - 0.5) * (gl_PointCoord.x-0.5) + (gl_PointCoord.y-0.5) * (gl_PointCoord.y-0.5) );\n" -" bool in_bounds;\n" -" switch(marker_type) {\n" -" case 1:\n" -" in_bounds = dist < 0.3;\n" -" break;\n" -" case 2:\n" -" in_bounds = ( (dist > 0.3) && (dist<0.5) );\n" -" break;\n" -" case 3:\n" -" in_bounds = ((gl_PointCoord.x < 0.15) || (gl_PointCoord.x > 0.85)) ||\n" -" ((gl_PointCoord.y < 0.15) || (gl_PointCoord.y > 0.85));\n" -" break;\n" -" case 4:\n" -" in_bounds = (2*(gl_PointCoord.x - 0.25) - (gl_PointCoord.y + 0.5) < 0) && (2*(gl_PointCoord.x - 0.25) + (gl_PointCoord.y + 0.5) > 1);\n" -" break;\n" -" case 5:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.13 ||\n" -" abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.13 ;\n" -" break;\n" -" case 6:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5)) < 0.07 ||\n" -" abs((gl_PointCoord.y - 0.5)) < 0.07;\n" -" break;\n" -" case 7:\n" -" in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.07 ||\n" -" abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.07 ||\n" -" abs((gl_PointCoord.x - 0.5)) < 0.07 ||\n" -" abs((gl_PointCoord.y - 0.5)) < 0.07;\n" -" break;\n" -" case 8:\n" -" in_bounds = true;\n" -" break;\n" -" default:\n" -" in_bounds = true;\n" -" }\n" -" if(!in_bounds)\n" -" discard;\n" -" else\n" -" outputColor = line_color;\n" -"}"; - +#include -namespace internal -{ +using namespace std; -void surface_impl::bindResources(int pWindowId) +void generateGridIndices(unsigned short rows, unsigned short cols, unsigned short *indices) { - if (mVAOMap.find(pWindowId) == mVAOMap.end()) { - GLuint vao = 0; - /* create a vertex array object - * with appropriate bindings */ - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - // attach plot vertices - glEnableVertexAttribArray(mPointIndex); - glBindBuffer(GL_ARRAY_BUFFER, mMainVBO); - glVertexAttribPointer(mPointIndex, 3, mDataType, GL_FALSE, 0, 0); - //attach indices - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexVBO); - glBindVertexArray(0); - /* store the vertex array object corresponding to - * the window instance in the map */ - mVAOMap[pWindowId] = vao; - } - - glBindVertexArray(mVAOMap[pWindowId]); -} - -void surface_impl::unbindResources() const { glBindVertexArray(0); } - -void generate_grid_indices(unsigned short rows, unsigned short cols, unsigned short *indices){ unsigned short idx = 0; for(unsigned short r = 0; r < rows-1; ++r){ for(unsigned short c = 0; c < cols*2; ++c){ @@ -148,179 +43,218 @@ void generate_grid_indices(unsigned short rows, unsigned short cols, unsigned sh } } -surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, - fg::dtype pDataType, fg::MarkerType pMarkerType) - : Chart3D(), mNumXPoints(pNumXPoints),mNumYPoints(pNumYPoints), - mDataType(gl_dtype(pDataType)), mMainVBO(0), mMainVBOsize(0), - mIndexVBO(0), mIndexVBOsize(0), mPointIndex(0), mMarkerTypeIndex(0), - mMarkerColIndex(0), mSpriteTMatIndex(0), mSurfPointIndex(0), - mSurfTMatIndex(0), mSurfRangeIndex(0) +namespace internal { - CheckGL("Begin surface_impl::surface_impl"); - mPointIndex = mBorderAttribPointIndex; - mMarkerType = pMarkerType; - mSurfProgram = initShaders(gMarkerVertexShaderSrc, gSurfFragmentShaderSrc); - mMarkerProgram = initShaders(gMarkerVertexShaderSrc, gMarkerSpriteFragmentShaderSrc); - - mSurfPointIndex = glGetAttribLocation (mSurfProgram, "point"); - mSurfTMatIndex = glGetUniformLocation(mSurfProgram, "transform"); - mSurfRangeIndex = glGetUniformLocation(mSurfProgram, "minmaxs"); - - mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); - mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "line_color"); - mSpriteTMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); - - unsigned total_points = 3*(mNumXPoints * mNumYPoints); - - mIndexVBOsize = (2 * mNumYPoints) * (mNumXPoints - 1); - unsigned short* indices = new unsigned short[mIndexVBOsize]; - generate_grid_indices(mNumXPoints, mNumYPoints, indices); - mIndexVBO = createBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexVBOsize, NULL, GL_STATIC_DRAW); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexVBO); - glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, mIndexVBOsize * sizeof(unsigned short), indices); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - delete[] indices; - - // buffersubdata calls on mMainVBO - // will only update the points data - switch(mDataType) { - case GL_FLOAT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(float); - break; - case GL_INT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(int); - break; - case GL_UNSIGNED_INT: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned); - break; - case GL_UNSIGNED_BYTE: - mMainVBO = createBuffer(GL_ARRAY_BUFFER, total_points, NULL, GL_DYNAMIC_DRAW); - mMainVBOsize = total_points*sizeof(unsigned char); - break; - default: fg::TypeError("Plot::Plot", __LINE__, 1, pDataType); - } - CheckGL("End surface_impl::surface_impl"); -} -surface_impl::~surface_impl() +void surface_impl::bindResources(const int pWindowId) { - CheckGL("Begin Plot::~Plot"); - glDeleteBuffers(1, &mMainVBO); - CheckGL("End Plot::~Plot"); -} + if (mVAOMap.find(pWindowId) == mVAOMap.end()) { + GLuint vao = 0; + /* create a vertex array object + * with appropriate bindings */ + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + // attach plot vertices + glEnableVertexAttribArray(mSurfPointIndex); + glBindBuffer(GL_ARRAY_BUFFER, mVBO); + glVertexAttribPointer(mSurfPointIndex, 3, mDataType, GL_FALSE, 0, 0); + glEnableVertexAttribArray(mSurfColorIndex); + glBindBuffer(GL_ARRAY_BUFFER, mCBO); + glVertexAttribPointer(mSurfColorIndex, 3, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(mSurfAlphaIndex); + glBindBuffer(GL_ARRAY_BUFFER, mABO); + glVertexAttribPointer(mSurfAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); + //attach indices + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIBO); + glBindVertexArray(0); + /* store the vertex array object corresponding to + * the window instance in the map */ + mVAOMap[pWindowId] = vao; + } -void surface_impl::setColor(fg::Color col) -{ - mLineColor[0] = (((int) col >> 24 ) & 0xFF ) / 255.f; - mLineColor[1] = (((int) col >> 16 ) & 0xFF ) / 255.f; - mLineColor[2] = (((int) col >> 8 ) & 0xFF ) / 255.f; - mLineColor[3] = (((int) col ) & 0xFF ) / 255.f; + glBindVertexArray(mVAOMap[pWindowId]); } -void surface_impl::setColor(float r, float g, float b) +void surface_impl::unbindResources() const { - mLineColor[0] = clampTo01(r); - mLineColor[1] = clampTo01(g); - mLineColor[2] = clampTo01(b); - mLineColor[3] = 1.0f; + glBindVertexArray(0); } -GLuint surface_impl::vbo() const { return mMainVBO; } - -size_t surface_impl::size() const { return mMainVBOsize; } - -void surface_impl::render(int pWindowId, int pX, int pY, int pVPW, int pVPH) +void surface_impl::computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput) { - float range_x = xmax() - xmin(); - float range_y = ymax() - ymin(); - float range_z = zmax() - zmin(); + // identity matrix + static const glm::mat4 I(1.0f); + static const glm::mat4 VIEW = glm::lookAt(glm::vec3(-1.f,0.5f, 1.f), + glm::vec3( 1.f,-1.f,-1.f), + glm::vec3( 0.f, 1.f, 0.f)); + static const glm::mat4 PROJECTION = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 100.f); + + float range_x = mRange[1] - mRange[0]; + float range_y = mRange[3] - mRange[2]; + float range_z = mRange[5] - mRange[4]; // set scale to zero if input is constant array // otherwise compute scale factor by standard equation - float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(xmax() - xmin()); - float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(ymax() - ymin()); - float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(zmax() - zmin()); - - CheckGL("Begin surface_impl::render"); + float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); + float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); + float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(range_z); - float coor_offset_x = ( -xmin() * graph_scale_x); - float coor_offset_y = ( -ymin() * graph_scale_y); - float coor_offset_z = ( -zmin() * graph_scale_z); + float coor_offset_x = ( -mRange[0] * graph_scale_x); + float coor_offset_y = ( -mRange[2] * graph_scale_y); + float coor_offset_z = (-mRange[4] * graph_scale_z); - glm::mat4 model = glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(0,1,0)) * glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(1,0,0)) * glm::translate(glm::mat4(1.f), glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)) * glm::scale(glm::mat4(1.f), glm::vec3(1.0f * graph_scale_x, 1.0f * graph_scale_y, 1.0f * graph_scale_z)); - glm::mat4 view = glm::lookAt(glm::vec3(-1,0.5f,1.0f), glm::vec3(1,-1,-1),glm::vec3(0,1,0)); - glm::mat4 projection = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 100.f); - glm::mat4 mvp = projection * view * model; - glm::mat4 transform = mvp; - renderGraph(pWindowId, transform); + glm::mat4 rMat = glm::rotate(I, + -glm::radians(90.f), glm::vec3(1,0,0)); + glm::mat4 tMat = glm::translate(I, + glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)); + glm::mat4 sMat = glm::scale(I, + glm::vec3(1.0f * graph_scale_x, -1.0f * graph_scale_y, 1.0f * graph_scale_z)); - /* render graph border and axes */ - renderChart(pWindowId, pX, pY, pVPW, pVPH); + glm::mat4 model = rMat * tMat * sMat; - CheckGL("End surface_impl::render"); + pOut = PROJECTION * VIEW * model; } -void surface_impl::renderGraph(int pWindowId, glm::mat4 transform) +void surface_impl::renderGraph(const int pWindowId, const glm::mat4& transform) { CheckGL("Begin surface_impl::renderGraph"); - bindSurfProgram(); - GLfloat range[] = {xmax(), xmin(), ymax(), ymin(), zmax(), zmin()}; - glUniform2fv(surfRangeIndex(), 3, range); - glUniformMatrix4fv(surfMatIndex(), 1, GL_FALSE, glm::value_ptr(transform)); + glUseProgram(mSurfProgram); + + glUniformMatrix4fv(mSurfMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); + glUniform2fv(mSurfRangeIndex, 3, mRange); + glUniform1i(mSurfPVCIndex, mIsPVCOn); bindResources(pWindowId); - glDrawElements(GL_TRIANGLE_STRIP, mIndexVBOsize, GL_UNSIGNED_SHORT, (void*)0 ); + glDrawElements(GL_TRIANGLE_STRIP, mIBOSize, GL_UNSIGNED_SHORT, (void*)0 ); unbindResources(); - unbindSurfProgram(); + glUseProgram(0); if(mMarkerType != fg::FG_NONE) { glEnable(GL_PROGRAM_POINT_SIZE); glUseProgram(mMarkerProgram); - glUniformMatrix4fv(spriteMatIndex(), 1, GL_FALSE, glm::value_ptr(transform)); - glUniform4fv(markerColIndex(), 1, WHITE); - glUniform1i(markerTypeIndex(), mMarkerType); + glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); + glUniform2fv(mMarkerRangeIndex, 3, mRange); + glUniform1i(mMarkerPVCIndex, mIsPVCOn); + glUniform1i(mMarkerTypeIndex, mMarkerType); + glUniform4fv(mMarkerColIndex, 1, mColor); bindResources(pWindowId); - glDrawElements(GL_POINTS, mIndexVBOsize, GL_UNSIGNED_SHORT, (void*)0 ); + glDrawElements(GL_POINTS, mIBOSize, GL_UNSIGNED_SHORT, (void*)0); unbindResources(); + glUseProgram(0); glDisable(GL_PROGRAM_POINT_SIZE); } CheckGL("End surface_impl::renderGraph"); } -GLuint surface_impl::markerTypeIndex() const { return mMarkerTypeIndex; } -GLuint surface_impl::spriteMatIndex() const { return mSpriteTMatIndex; } +surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, + fg::dtype pDataType, fg::MarkerType pMarkerType) + : mNumXPoints(pNumXPoints),mNumYPoints(pNumYPoints), mDataType(dtype2gl(pDataType)), + mIsPVCOn(false), mMarkerType(pMarkerType), mIBO(0), mIBOSize(0), mMarkerProgram(-1), + mSurfProgram(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), + mMarkerAlphaIndex(-1), mMarkerPVCIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), + mSurfMatIndex(-1), mSurfRangeIndex(-1), mSurfPointIndex(-1), mSurfColorIndex(-1), + mSurfAlphaIndex(-1), mSurfPVCIndex(-1) +{ + CheckGL("Begin surface_impl::surface_impl"); + mColor[0] = 0.9f; + mColor[1] = 0.5f; + mColor[2] = 0.6f; + mColor[3] = 1.0f; + + mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); + mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); + mMarkerRangeIndex= glGetUniformLocation(mMarkerProgram, "minmaxs"); + mMarkerPointIndex= glGetUniformLocation(mMarkerProgram, "point"); + mMarkerColorIndex= glGetUniformLocation(mMarkerProgram, "color"); + mMarkerAlphaIndex= glGetUniformLocation(mMarkerProgram, "alpha"); + mMarkerPVCIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); + mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); + mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); + + mSurfProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); + mSurfMatIndex = glGetUniformLocation(mSurfProgram, "transform"); + mSurfRangeIndex = glGetUniformLocation(mSurfProgram, "minmaxs"); + mSurfPointIndex = glGetUniformLocation(mSurfProgram, "point"); + mSurfColorIndex = glGetUniformLocation(mSurfProgram, "color"); + mSurfAlphaIndex = glGetUniformLocation(mSurfProgram, "alpha"); + mSurfPVCIndex = glGetUniformLocation(mSurfProgram, "isPVCOn"); + + unsigned totalPoints = mNumXPoints * mNumYPoints; + + mVBOSize = 3*totalPoints; + mCBOSize = 3*totalPoints; + mABOSize = totalPoints; +#define SURF_CREATE_BUFFERS(type) \ + mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ + mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ + mABO = createBuffer(GL_ARRAY_BUFFER, mABOSize, NULL, GL_DYNAMIC_DRAW); \ + mVBOSize *= sizeof(type); \ + mCBOSize *= sizeof(float); \ + mABOSize *= sizeof(float); -GLuint surface_impl::markerColIndex() const { return mMarkerColIndex; } + switch(mDataType) { + case GL_FLOAT : SURF_CREATE_BUFFERS(float) ; break; + case GL_INT : SURF_CREATE_BUFFERS(int) ; break; + case GL_UNSIGNED_INT : SURF_CREATE_BUFFERS(uint) ; break; + case GL_SHORT : SURF_CREATE_BUFFERS(short) ; break; + case GL_UNSIGNED_SHORT : SURF_CREATE_BUFFERS(ushort); break; + case GL_UNSIGNED_BYTE : SURF_CREATE_BUFFERS(float) ; break; + default: fg::TypeError("surface_impl::surface_impl", __LINE__, 1, pDataType); + } -GLuint surface_impl::surfMatIndex() const { return mSurfTMatIndex; } +#undef SURF_CREATE_BUFFERS -GLuint surface_impl::surfRangeIndex() const { return mSurfRangeIndex; } + mIBOSize = (2 * mNumYPoints) * (mNumXPoints - 1); + std::vector indices(mIBOSize); + generateGridIndices(mNumXPoints, mNumYPoints, indices.data()); + mIBO = createBuffer(GL_ELEMENT_ARRAY_BUFFER, mIBOSize, indices.data(), GL_STATIC_DRAW); -void surface_impl::bindSurfProgram() const { glUseProgram(mSurfProgram); } + CheckGL("End surface_impl::surface_impl"); +} -void surface_impl::unbindSurfProgram() const { glUseProgram(0); } +surface_impl::~surface_impl() +{ + CheckGL("Begin Plot::~Plot"); + glDeleteBuffers(1, &mVBO); + glDeleteBuffers(1, &mCBO); + glDeleteBuffers(1, &mABO); + glDeleteBuffers(1, &mIBO); + glDeleteProgram(mMarkerProgram); + glDeleteProgram(mSurfProgram); + CheckGL("End Plot::~Plot"); +} +void surface_impl::render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4 &pModel) +{ + CheckGL("Begin surface_impl::render"); + glm::mat4 mvp(1.0); + computeTransformMat(mvp, pModel); + renderGraph(pWindowId, mvp); + CheckGL("End surface_impl::render"); +} -void scatter3_impl::renderGraph(int pWindowId, glm::mat4 transform) +void scatter3_impl::renderGraph(const int pWindowId, const glm::mat4& transform) { if(mMarkerType != fg::FG_NONE) { glEnable(GL_PROGRAM_POINT_SIZE); glUseProgram(mMarkerProgram); - glUniformMatrix4fv(spriteMatIndex(), 1, GL_FALSE, glm::value_ptr(transform)); - glUniform4fv(markerColIndex(), 1, mLineColor); - glUniform1i(markerTypeIndex(), mMarkerType); + glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); + glUniform2fv(mMarkerRangeIndex, 3, mRange); + glUniform1i(mMarkerPVCIndex, mIsPVCOn); + glUniform1i(mMarkerTypeIndex, mMarkerType); + glUniform4fv(mMarkerColIndex, 1, mColor); bindResources(pWindowId); - glDrawElements(GL_POINTS, mIndexVBOsize, GL_UNSIGNED_SHORT, (void*)0); + glDrawElements(GL_POINTS, mIBOSize, GL_UNSIGNED_SHORT, (void*)0); unbindResources(); + glUseProgram(0); glDisable(GL_PROGRAM_POINT_SIZE); } @@ -333,82 +267,72 @@ namespace fg Surface::Surface(unsigned pNumXPoints, unsigned pNumYPoints, dtype pDataType, PlotType pPlotType, MarkerType pMarkerType) { - value = new internal::_Surface(pNumXPoints, pNumYPoints, pDataType, pPlotType, pMarkerType); + mValue = new internal::_Surface(pNumXPoints, pNumYPoints, pDataType, pPlotType, pMarkerType); } Surface::Surface(const Surface& other) { - value = new internal::_Surface(*other.get()); + mValue = new internal::_Surface(*other.get()); } Surface::~Surface() { - delete value; -} - -void Surface::setColor(fg::Color col) -{ - value->setColor(col); -} - -void Surface::setColor(float r, float g, float b) -{ - value->setColor(r, g, b); -} - -void Surface::setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, float pZmax, float pZmin) -{ - value->setAxesLimits(pXmax, pXmin, pYmax, pYmin, pZmax, pZmin); + delete mValue; } -void Surface::setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle) +void Surface::setColor(const Color pColor) { - value->setAxesTitles(pXTitle, pYTitle, pZTitle); + float r = (((int) pColor >> 24 ) & 0xFF ) / 255.f; + float g = (((int) pColor >> 16 ) & 0xFF ) / 255.f; + float b = (((int) pColor >> 8 ) & 0xFF ) / 255.f; + float a = (((int) pColor ) & 0xFF ) / 255.f; + mValue->setColor(r, g, b, a); } -float Surface::xmax() const +void Surface::setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { - return value->xmax(); + mValue->setColor(pRed, pGreen, pBlue, pAlpha); } -float Surface::xmin() const +void Surface::setLegend(const std::string& pLegend) { - return value->xmin(); + mValue->setLegend(pLegend); } -float Surface::ymax() const +uint Surface::vertices() const { - return value->ymax(); + return mValue->vbo(); } -float Surface::ymin() const +uint Surface::colors() const { - return value->ymin(); + return mValue->cbo(); } -float Surface::zmax() const +uint Surface::alphas() const { - return value->zmax(); + return mValue->abo(); } -float Surface::zmin() const +uint Surface::verticesSize() const { - return value->zmin(); + return (uint)mValue->vboSize(); } -unsigned Surface::vbo() const +uint Surface::colorsSize() const { - return value->vbo(); + return (uint)mValue->cboSize(); } -unsigned Surface::size() const +uint Surface::alphasSize() const { - return (unsigned)value->size(); + return (uint)mValue->aboSize(); } internal::_Surface* Surface::get() const { - return value; + return mValue; } } diff --git a/src/surface.hpp b/src/surface.hpp index 7f0288de..54a1159c 100644 --- a/src/surface.hpp +++ b/src/surface.hpp @@ -10,145 +10,137 @@ #pragma once #include -#include + +#include + #include #include -#include namespace internal { -class surface_impl : public Chart3D { +class surface_impl : public AbstractRenderable { protected: /* plot points characteristics */ GLuint mNumXPoints; GLuint mNumYPoints; GLenum mDataType; - float mLineColor[4]; + bool mIsPVCOn; fg::MarkerType mMarkerType; /* OpenGL Objects */ - GLuint mMainVBO; - size_t mMainVBOsize; - GLuint mIndexVBO; - size_t mIndexVBOsize; + GLuint mIBO; + size_t mIBOSize; GLuint mMarkerProgram; GLuint mSurfProgram; /* shared variable index locations */ - GLuint mPointIndex; + GLuint mMarkerMatIndex; + GLuint mMarkerRangeIndex; + GLuint mMarkerPointIndex; + GLuint mMarkerColorIndex; + GLuint mMarkerAlphaIndex; + GLuint mMarkerPVCIndex; GLuint mMarkerTypeIndex; GLuint mMarkerColIndex; - GLuint mSpriteTMatIndex; - GLuint mSurfPointIndex; - GLuint mSurfTMatIndex; + + GLuint mSurfMatIndex; GLuint mSurfRangeIndex; + GLuint mSurfPointIndex; + GLuint mSurfColorIndex; + GLuint mSurfAlphaIndex; + GLuint mSurfPVCIndex; + + float mRange[6]; std::map mVAOMap; /* bind and unbind helper functions * for rendering resources */ - void bindResources(int pWindowId); + void bindResources(const int pWindowId); void unbindResources() const; - void bindSurfProgram() const; - void unbindSurfProgram() const; - GLuint markerTypeIndex() const; - GLuint spriteMatIndex() const; - GLuint markerColIndex() const; - GLuint surfRangeIndex() const; - GLuint surfMatIndex() const; - virtual void renderGraph(int pWindowId, glm::mat4 transform); + void computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput); + virtual void renderGraph(const int pWindowId, const glm::mat4& transform); public: - surface_impl(unsigned pNumXpoints, unsigned pNumYpoints, fg::dtype pDataType, fg::MarkerType pMarkerType); + surface_impl(const uint pNumXpoints, const uint pNumYpoints, + const fg::dtype pDataType, const fg::MarkerType pMarkerType); ~surface_impl(); - void setColor(fg::Color col); - void setColor(float r, float g, float b); - GLuint vbo() const; - size_t size() const; - - void render(int pWindowId, int pX, int pY, int pViewPortWidth, int pViewPortHeight); + void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4 &pTransform); }; class scatter3_impl : public surface_impl { private: - void renderGraph(int pWindowId, glm::mat4 transform); + void renderGraph(const int pWindowId, const glm::mat4& transform); public: - scatter3_impl(unsigned pNumXPoints, unsigned pNumYPoints, fg::dtype pDataType, fg::MarkerType pMarkerType=fg::FG_NONE) + scatter3_impl(const uint pNumXPoints, const uint pNumYPoints, + const fg::dtype pDataType, const fg::MarkerType pMarkerType=fg::FG_NONE) : surface_impl(pNumXPoints, pNumYPoints, pDataType, pMarkerType) {} - - ~scatter3_impl() {} }; class _Surface { private: - std::shared_ptr plt; + std::shared_ptr mSurface; public: - _Surface(unsigned pNumXPoints, unsigned pNumYPoints, fg::dtype pDataType, fg::PlotType pPlotType=fg::FG_SURFACE, fg::MarkerType pMarkerType=fg::FG_NONE) { + _Surface(const uint pNumXPoints, const uint pNumYPoints, + const fg::dtype pDataType, const fg::PlotType pPlotType=fg::FG_SURFACE, + const fg::MarkerType pMarkerType=fg::FG_NONE) { switch(pPlotType){ case(fg::FG_SURFACE): - plt = std::make_shared(pNumXPoints, pNumYPoints, pDataType, pMarkerType); + mSurface = std::make_shared(pNumXPoints, pNumYPoints, pDataType, pMarkerType); break; case(fg::FG_SCATTER): - plt = std::make_shared(pNumXPoints, pNumYPoints, pDataType, pMarkerType); + mSurface = std::make_shared(pNumXPoints, pNumYPoints, pDataType, pMarkerType); break; default: - plt = std::make_shared(pNumXPoints, pNumYPoints, pDataType, pMarkerType); + mSurface = std::make_shared(pNumXPoints, pNumYPoints, pDataType, pMarkerType); }; } inline const std::shared_ptr& impl() const { - return plt; - } - - inline void setColor(fg::Color col) { - plt->setColor(col); - } - - inline void setColor(float r, float g, float b) { - plt->setColor(r, g, b); + return mSurface; } - inline void setAxesLimits(float pXmax, float pXmin, float pYmax, float pYmin, float pZmax, float pZmin) { - plt->setAxesLimits(pXmax, pXmin, pYmax, pYmin, pZmax, pZmin); + inline void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { + mSurface->setColor(pRed, pGreen, pBlue, pAlpha); } - inline void setAxesTitles(const char* pXTitle, const char* pYTitle, const char* pZTitle) - { - plt->setAxesTitles(pXTitle, pYTitle, pZTitle); + inline void setLegend(const std::string& pLegend) { + mSurface->setLegend(pLegend); } - inline float xmax() const { - return plt->xmax(); - } - - inline float xmin() const { - return plt->xmin(); + inline GLuint vbo() const { + return mSurface->vbo(); } - inline float ymax() const { - return plt->ymax(); + inline GLuint cbo() const { + return mSurface->cbo(); } - inline float ymin() const { - return plt->ymin(); + inline GLuint abo() const { + return mSurface->abo(); } - inline float zmax() const { - return plt->zmax(); + inline size_t vboSize() const { + return mSurface->vboSize(); } - inline float zmin() const { - return plt->zmin(); + inline size_t cboSize() const { + return mSurface->cboSize(); } - inline GLuint vbo() const { - return plt->vbo(); + inline size_t aboSize() const { + return mSurface->aboSize(); } - inline size_t size() const { - return plt->size(); + inline void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) const { + mSurface->render(pWindowId, pX, pY, pVPW, pVPH, pTransform); } }; diff --git a/src/window.cpp b/src/window.cpp index 46000958..34a60117 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -172,6 +172,11 @@ void window_impl::setColorMap(fg::ColorMap cmap) } } +int window_impl::getID() const +{ + return mID; +} + long long window_impl::context() const { return mCxt; @@ -237,7 +242,7 @@ void window_impl::draw(const std::shared_ptr& pRenderable) glClearColor(GRAY[0], GRAY[1], GRAY[2], GRAY[3]); pRenderable->setColorMapUBOParams(mColorMapUBO, mUBOSize); - pRenderable->render(mID, 0, 0, wind_width, wind_height); + pRenderable->render(mID, 0, 0, wind_width, wind_height, glm::mat4(1.0f)); mWindow->swapBuffers(); mWindow->pollEvents(); @@ -292,7 +297,7 @@ void window_impl::draw(int pColId, int pRowId, glClearColor(GRAY[0], GRAY[1], GRAY[2], GRAY[3]); pRenderable->setColorMapUBOParams(mColorMapUBO, mUBOSize); - pRenderable->render(mID, x_off, y_off, mCellWidth, mCellHeight); + pRenderable->render(mID, x_off, y_off, mCellWidth, mCellHeight, glm::mat4(1.0f)); glDisable(GL_SCISSOR_TEST); glViewport(x_off, y_off, mCellWidth, mCellHeight); @@ -323,151 +328,120 @@ namespace fg Window::Window(int pWidth, int pHeight, const char* pTitle, const Window* pWindow, const bool invisible) { if (pWindow == nullptr) { - value = new internal::_Window(pWidth, pHeight, pTitle, nullptr, invisible); + mValue = new internal::_Window(pWidth, pHeight, pTitle, nullptr, invisible); } else { - value = new internal::_Window(pWidth, pHeight, pTitle, pWindow->get(), invisible); + mValue = new internal::_Window(pWidth, pHeight, pTitle, pWindow->get(), invisible); } } Window::~Window() { - delete value; + delete mValue; } Window::Window(const Window& other) { - value = new internal::_Window(*other.get()); + mValue = new internal::_Window(*other.get()); } void Window::setFont(Font* pFont) { - value->setFont(pFont->get()); + mValue->setFont(pFont->get()); } void Window::setTitle(const char* pTitle) { - value->setTitle(pTitle); + mValue->setTitle(pTitle); } void Window::setPos(int pX, int pY) { - value->setPos(pX, pY); + mValue->setPos(pX, pY); } void Window::setSize(unsigned pW, unsigned pH) { - value->setSize(pW, pH); + mValue->setSize(pW, pH); } void Window::setColorMap(ColorMap cmap) { - value->setColorMap(cmap); + mValue->setColorMap(cmap); } long long Window::context() const { - return value->context(); + return mValue->context(); } long long Window::display() const { - return value->display(); + return mValue->display(); } int Window::width() const { - return value->width(); + return mValue->width(); } int Window::height() const { - return value->height(); + return mValue->height(); } internal::_Window* Window::get() const { - return value; + return mValue; } void Window::hide() { - value->hide(); + mValue->hide(); } void Window::show() { - value->show(); + mValue->show(); } bool Window::close() { - return value->close(); + return mValue->close(); } void Window::makeCurrent() { - value->makeCurrent(); + mValue->makeCurrent(); } void Window::draw(const Image& pImage, const bool pKeepAspectRatio) { - value->draw(pImage.get(), pKeepAspectRatio); -} - -void Window::draw(const Plot& pPlot) -{ - value->draw(pPlot.get()); -} - -void Window::draw(const Plot3& pPlot3) -{ - value->draw(pPlot3.get()); -} - -void Window::draw(const Surface& pSurface) -{ - value->draw(pSurface.get()); + mValue->draw(pImage.get(), pKeepAspectRatio); } -void Window::draw(const Histogram& pHist) +void Window::draw(const Chart& pChart) { - value->draw(pHist.get()); + mValue->draw(pChart.get()); } void Window::grid(int pRows, int pCols) { - value->grid(pRows, pCols); + mValue->grid(pRows, pCols); } void Window::draw(int pColId, int pRowId, const Image& pImage, const char* pTitle, const bool pKeepAspectRatio) { - value->draw(pColId, pRowId, pImage.get(), pTitle, pKeepAspectRatio); -} - -void Window::draw(int pColId, int pRowId, const Plot& pPlot, const char* pTitle) -{ - value->draw(pColId, pRowId, pPlot.get(), pTitle); + mValue->draw(pColId, pRowId, pImage.get(), pTitle, pKeepAspectRatio); } -void Window::draw(int pColId, int pRowId, const Plot3& pPlot3, const char* pTitle) -{ - value->draw(pColId, pRowId, pPlot3.get(), pTitle); -} - -void Window::draw(int pColId, int pRowId, const Surface& pSurface, const char* pTitle) -{ - value->draw(pColId, pRowId, pSurface.get(), pTitle); -} - - -void Window::draw(int pColId, int pRowId, const Histogram& pHist, const char* pTitle) +void Window::draw(int pColId, int pRowId, const Chart& pChart, const char* pTitle) { - value->draw(pColId, pRowId, pHist.get(), pTitle); + mValue->draw(pColId, pRowId, pChart.get(), pTitle); } void Window::swapBuffers() { - value->swapBuffers(); + mValue->swapBuffers(); } } diff --git a/src/window.hpp b/src/window.hpp index c4153dfa..e51ee850 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -20,10 +20,7 @@ #include #include #include -#include -#include -#include -#include +#include #include @@ -62,6 +59,7 @@ class window_impl { void setSize(unsigned pWidth, unsigned pHeight); void setColorMap(fg::ColorMap cmap); + int getID() const; long long context() const; long long display() const; int width() const; @@ -89,7 +87,7 @@ void MakeContextCurrent(const window_impl* pWindow); class _Window { private: - std::shared_ptr wnd; + std::shared_ptr mWindow; _Window() {} @@ -98,108 +96,100 @@ class _Window { _Window(int pWidth, int pHeight, const char* pTitle, const _Window* pWindow, const bool invisible = false) { if (pWindow) { - wnd = std::make_shared(pWidth, pHeight, pTitle, + mWindow = std::make_shared(pWidth, pHeight, pTitle, pWindow->impl(), invisible); } else { std::shared_ptr other; - wnd = std::make_shared(pWidth, pHeight, pTitle, + mWindow = std::make_shared(pWidth, pHeight, pTitle, other, invisible); } } inline const std::shared_ptr& impl () const { - return wnd; + return mWindow; } inline void setFont (_Font* pFont) { - wnd->setFont (pFont->impl()); + mWindow->setFont (pFont->impl()); } inline void setTitle(const char* pTitle) { - wnd->setTitle(pTitle); + mWindow->setTitle(pTitle); } inline void setPos(int pX, int pY) { - wnd->setPos(pX, pY); + mWindow->setPos(pX, pY); } inline void setSize(unsigned pWidth, int pHeight) { - wnd->setSize(pWidth, pHeight); + mWindow->setSize(pWidth, pHeight); } inline void setColorMap(fg::ColorMap cmap) { - wnd->setColorMap(cmap); + mWindow->setColorMap(cmap); + } + + inline int getID() const { + return mWindow->getID(); } inline long long context() const { - return wnd->context() ; + return mWindow->context() ; } inline long long display() const { - return wnd->display(); + return mWindow->display(); } inline int width() const { - return wnd->width(); + return mWindow->width(); } inline int height() const { - return wnd->height(); + return mWindow->height(); } inline void makeCurrent() { - MakeContextCurrent(wnd.get()); + MakeContextCurrent(mWindow.get()); } inline void hide() { - wnd->hide(); + mWindow->hide(); } inline void show() { - wnd->show(); + mWindow->show(); } inline bool close() { - return wnd->close(); + return mWindow->close(); } inline void draw(_Image* pImage, const bool pKeepAspectRatio) { pImage->keepAspectRatio(pKeepAspectRatio); - wnd->draw(pImage->impl()) ; - } - - inline void draw(const _Plot* pPlot) { - wnd->draw(pPlot->impl()) ; - } - - inline void draw(const _Plot3* pPlot3) { - wnd->draw(pPlot3->impl()) ; - } - - inline void draw(const _Surface* pSurface) { - wnd->draw(pSurface->impl()) ; + mWindow->draw(pImage->impl()) ; } - inline void draw(const _Histogram* pHist) { - wnd->draw(pHist->impl()) ; + inline void draw(const _Chart* pChart) { + mWindow->draw(pChart->impl()) ; } inline void swapBuffers() { - wnd->swapBuffers(); + mWindow->swapBuffers(); } inline void grid(int pRows, int pCols) { - wnd->grid(pRows, pCols); + mWindow->grid(pRows, pCols); } template void draw(int pColId, int pRowId, T* pRenderable, const char* pTitle) { - wnd->draw(pColId, pRowId, pRenderable->impl(), pTitle); + mWindow->draw(pColId, pRowId, pRenderable->impl(), pTitle); } void draw(int pColId, int pRowId, _Image* pRenderable, const char* pTitle, const bool pKeepAspectRatio) { pRenderable->keepAspectRatio(pKeepAspectRatio); - wnd->draw(pColId, pRowId, pRenderable->impl(), pTitle); + mWindow->draw(pColId, pRowId, pRenderable->impl(), pTitle); } }; From c4595392c5310823af43734e661a5fcdde6e0ba3 Mon Sep 17 00:00:00 2001 From: pradeep Date: Tue, 19 Jan 2016 20:17:08 +0530 Subject: [PATCH 02/61] code reorganization progress update Debugging following issues: * histogram rendering seems to be buggy * 3d line plot and surface are not rendering anything, though the 3d charts by themselves are rendering fine. Finished: * GLSL shader to std::string headers * Ported Image class to new framework, image examples working fine * 2d line and scatter plots working fine, multiple plots per chart also working fine. * Ported histogram, 3d line plot and histogram to new framework. --- examples/cpu/plot3.cpp | 8 ++-- examples/cpu/plotting.cpp | 33 +++++++++++------ examples/cuda/plot3.cu | 1 - examples/cuda/plotting.cu | 54 +++++++++++++++++++-------- examples/opencl/plot3.cpp | 1 - examples/opencl/plotting.cpp | 64 +++++++++++++++++++++----------- src/chart.cpp | 25 +++++++------ src/histogram.cpp | 31 +++++++++------- src/histogram.hpp | 2 - src/plot.hpp | 72 +++++++++++++++++------------------- src/surface.cpp | 33 +++++++---------- src/surface.hpp | 2 - 12 files changed, 184 insertions(+), 142 deletions(-) diff --git a/examples/cpu/plot3.cpp b/examples/cpu/plot3.cpp index de982908..05d1a7f8 100644 --- a/examples/cpu/plot3.cpp +++ b/examples/cpu/plot3.cpp @@ -24,7 +24,9 @@ const float DX = 0.005; const size_t ZSIZE = (ZMAX-ZMIN)/DX+1; using namespace std; -void gen_curve(float t, float dx, std::vector &vec ) { + +void gen_curve(float t, float dx, std::vector &vec ) +{ vec.clear(); for(float z=ZMIN; z < ZMAX; z+=dx){ vec.push_back(cos(z*t+t)/z); @@ -33,7 +35,8 @@ void gen_curve(float t, float dx, std::vector &vec ) { } } -int main(void){ +int main(void) +{ /* * First Forge call should be a window creation call * so that necessary OpenGL context is created for any @@ -74,7 +77,6 @@ int main(void){ t+=0.01; gen_curve(t, DX, function); copy(plot3, &function[0]); - // draw window and poll for events last wnd.draw(chart); } while(!wnd.close()); diff --git a/examples/cpu/plotting.cpp b/examples/cpu/plotting.cpp index 03d3e605..075b8b12 100644 --- a/examples/cpu/plotting.cpp +++ b/examples/cpu/plotting.cpp @@ -21,7 +21,10 @@ const float FRANGE_START = 0.f; const float FRANGE_END = 2.f * 3.1415926f; using namespace std; -void map_range_to_vec_vbo(float range_start, float range_end, float dx, std::vector &vec, float (*map) (float)){ +void map_range_to_vec_vbo(float range_start, float range_end, float dx, + std::vector &vec, + float (*map) (float)) +{ if(range_start > range_end && dx > 0) return; for(float i=range_start; i < range_end; i+=dx){ vec.push_back(i); @@ -31,8 +34,14 @@ void map_range_to_vec_vbo(float range_start, float range_end, float dx, std::vec int main(void) { - std::vector function; - map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, function, &sinf); + std::vector sinData; + std::vector cosData; + std::vector tanData; + std::vector logData; + map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, sinData, &sinf); + map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, cosData, &cosf); + map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, tanData, &tanf); + map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, logData, &log10f); /* * First Forge call should be a window creation call @@ -58,10 +67,11 @@ int main(void) /* Create several plot objects which creates the necessary * vertex buffer objects to hold the different plot types */ - fg::Plot plt0 = chart.plot(function.size()/2, fg::f32); //create a default plot - fg::Plot plt1 = chart.plot(function.size()/2, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type - fg::Plot plt2 = chart.plot(function.size()/2, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape - fg::Plot plt3 = chart.plot(function.size()/2, fg::f32, fg::FG_SCATTER, fg::FG_POINT); + fg::Plot plt0 = chart.plot(sinData.size()/2, fg::f32); //create a default plot + fg::Plot plt1 = chart.plot(cosData.size()/2, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type + fg::Plot plt2 = chart.plot(tanData.size()/2, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape + fg::Plot plt3 = chart.plot(logData.size()/2, fg::f32, fg::FG_SCATTER, fg::FG_POINT); + /* * Set plot colors @@ -78,14 +88,13 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - copy(plt0, &function[0]); - copy(plt1, &function[0]); - copy(plt2, &function[0]); - copy(plt3, &function[0]); + copy(plt0, &sinData[0]); + copy(plt1, &cosData[0]); + copy(plt2, &tanData[0]); + copy(plt3, &logData[0]); do { wnd.draw(chart); - wnd.swapBuffers(); } while(!wnd.close()); return 0; diff --git a/examples/cuda/plot3.cu b/examples/cuda/plot3.cu index fd5a7d82..228f2644 100644 --- a/examples/cuda/plot3.cu +++ b/examples/cuda/plot3.cu @@ -69,7 +69,6 @@ int main(void) t+=0.01; kernel(t, DX, dev_out); fg::copy(plot3, dev_out); - // draw window and poll for events last wnd.draw(chart); } while(!wnd.close()); diff --git a/examples/cuda/plotting.cu b/examples/cuda/plotting.cu index c1706d43..06032527 100644 --- a/examples/cuda/plotting.cu +++ b/examples/cuda/plotting.cu @@ -13,11 +13,14 @@ static const float FRANGE_START = 0.f; static const float FRANGE_END = 2 * 3.141592f; static const size_t DATA_SIZE = ( FRANGE_END - FRANGE_START ) / dx; -void kernel(float* dev_out); +void kernel(float* dev_out, int functionCode); int main(void) { - float *dev_out; + float *sin_out; + float *cos_out; + float *tan_out; + float *log_out; /* * First Forge call should be a window creation call @@ -57,37 +60,58 @@ int main(void) plt2.setColor(fg::FG_WHITE); //use a forge predefined color plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color - CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, sizeof(float) * DATA_SIZE * 2)); - kernel(dev_out); + CUDA_ERROR_CHECK(cudaMalloc((void**)&sin_out, sizeof(float) * DATA_SIZE * 2)); + CUDA_ERROR_CHECK(cudaMalloc((void**)&cos_out, sizeof(float) * DATA_SIZE * 2)); + CUDA_ERROR_CHECK(cudaMalloc((void**)&tan_out, sizeof(float) * DATA_SIZE * 2)); + CUDA_ERROR_CHECK(cudaMalloc((void**)&log_out, sizeof(float) * DATA_SIZE * 2)); + + kernel(sin_out, 0); + kernel(cos_out, 1); + kernel(tan_out, 2); + kernel(log_out, 3); /* copy your data into the vertex buffer object exposed by * fg::Plot class and then proceed to rendering. * To help the users with copying the data from compute * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(plt0, dev_out); - fg::copy(plt1, dev_out); - fg::copy(plt2, dev_out); - fg::copy(plt3, dev_out); + fg::copy(plt0, sin_out); + fg::copy(plt1, cos_out); + fg::copy(plt2, tan_out); + fg::copy(plt3, log_out); do { wnd.draw(chart); - wnd.swapBuffers(); } while(!wnd.close()); - CUDA_ERROR_CHECK(cudaFree(dev_out)); + CUDA_ERROR_CHECK(cudaFree(sin_out)); + CUDA_ERROR_CHECK(cudaFree(cos_out)); + CUDA_ERROR_CHECK(cudaFree(tan_out)); + CUDA_ERROR_CHECK(cudaFree(log_out)); return 0; } - __global__ -void simple_sinf(float* out, const size_t DATA_SIZE, const float dx) +void simple_sinf(float* out, const size_t DATA_SIZE, int fnCode) { int x = blockIdx.x * blockDim.x + threadIdx.x; if (x> >(dev_out, DATA_SIZE, dx); + simple_sinf << < blocks, threads >> >(dev_out, DATA_SIZE, functionCode); } diff --git a/examples/opencl/plot3.cpp b/examples/opencl/plot3.cpp index 232b0e7b..a25286fa 100644 --- a/examples/opencl/plot3.cpp +++ b/examples/opencl/plot3.cpp @@ -158,7 +158,6 @@ int main(void) t+=0.01; kernel(devOut, queue, t); fg::copy(plot3, devOut, queue); - // draw window and poll for events last wnd.draw(chart); } while(!wnd.close()); }catch (fg::Error err) { diff --git a/examples/opencl/plotting.cpp b/examples/opencl/plotting.cpp index fac7599d..f7976335 100644 --- a/examples/opencl/plotting.cpp +++ b/examples/opencl/plotting.cpp @@ -30,17 +30,31 @@ const float FRANGE_START = 0.f; const float FRANGE_END = 2 * 3.141592f; const unsigned DATA_SIZE = ( FRANGE_END - FRANGE_START ) / dx; -static const std::string sinf_ocl_kernel = -"kernel void sinf(global float* out, const float dx, const unsigned DATA_SIZE)\n" -"{\n" -" unsigned x = get_global_id(0);\n" -" if(x < DATA_SIZE){\n" -" out[2 * x] = x * dx ;\n" -" out[2 * x + 1] = sin(x*dx);\n" -" }\n" -"}\n"; - -void kernel(cl::Buffer& devOut, cl::CommandQueue& queue) +static const std::string sinf_ocl_kernel = R"( +kernel void sinf(global float* out, const float dx, const unsigned DATA_SIZE, int fnCode) +{ + unsigned x = get_global_id(0); + if(x < DATA_SIZE) { + out[2 * x] = x * dx ; + switch(fnCode) { + case 0: + out[ 2 * x + 1 ] = sin(x*dx); + break; + case 1: + out[ 2 * x + 1 ] = cos(x*dx); + break; + case 2: + out[ 2 * x + 1 ] = tan(x*dx); + break; + case 3: + out[ 2 * x + 1 ] = log10(x*dx); + break; + } + } +} +)"; + +void kernel(cl::Buffer& devOut, cl::CommandQueue& queue, int fnCode) { static std::once_flag compileFlag; static cl::Program prog; @@ -57,6 +71,7 @@ void kernel(cl::Buffer& devOut, cl::CommandQueue& queue) kern.setArg(0, devOut); kern.setArg(1, dx); kern.setArg(2, DATA_SIZE); + kern.setArg(3, fnCode); queue.enqueueNDRangeKernel(kern, cl::NullRange, global); } @@ -95,10 +110,10 @@ int main(void) /* * Set plot colors */ - plt0.setColor(fg::FG_YELLOW); - plt1.setColor(fg::FG_BLUE); - plt2.setColor(fg::FG_WHITE); //use a forge predefined color - plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + plt0.setColor(fg::FG_BLUE); + plt1.setColor(fg::FG_YELLOW); + plt2.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + plt3.setColor(fg::FG_WHITE); //use a forge predefined color Platform plat = getPlatform(); // Select the default platform and create a context using this platform and the GPU @@ -144,8 +159,14 @@ int main(void) } } - cl::Buffer devOut(context, CL_MEM_READ_WRITE, sizeof(float) * DATA_SIZE * 2); - kernel(devOut, queue); + cl::Buffer sinOut(context, CL_MEM_READ_WRITE, sizeof(float) * DATA_SIZE * 2); + cl::Buffer cosOut(context, CL_MEM_READ_WRITE, sizeof(float) * DATA_SIZE * 2); + cl::Buffer tanOut(context, CL_MEM_READ_WRITE, sizeof(float) * DATA_SIZE * 2); + cl::Buffer logOut(context, CL_MEM_READ_WRITE, sizeof(float) * DATA_SIZE * 2); + kernel(sinOut, queue, 0); + kernel(cosOut, queue, 1); + kernel(tanOut, queue, 2); + kernel(logOut, queue, 3); /* copy your data into the vertex buffer object exposed by * fg::Plot class and then proceed to rendering. @@ -153,14 +174,13 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(plt0, devOut, queue); - fg::copy(plt1, devOut, queue); - fg::copy(plt2, devOut, queue); - fg::copy(plt3, devOut, queue); + fg::copy(plt0, sinOut, queue); + fg::copy(plt1, cosOut, queue); + fg::copy(plt2, tanOut, queue); + fg::copy(plt3, logOut, queue); do { wnd.draw(chart); - wnd.swapBuffers(); } while(!wnd.close()); }catch (fg::Error err) { std::cout << err.what() << "(" << err.err() << ")" << std::endl; diff --git a/src/chart.cpp b/src/chart.cpp index 828cece4..52f7d8a8 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -406,12 +406,10 @@ void chart2d_impl::render(const int pWindowId, pos[1] += (mTickSize * (h/pVPH)); fonter->render(pWindowId, pos, WHITE, mXTitle.c_str(), CHART2D_FONT_SIZE); } - /* render all the renderables */ - // FIXME create the correct transformation matrix - glm::mat4 transMat = glm::mat4(1); + for (auto renderable : mRenderables) { renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); - renderable->render(pWindowId, pX, pY, pVPW, pVPH, transMat); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, trans); } CheckGL("End chart2d_impl::renderChart"); @@ -604,12 +602,17 @@ void chart3d_impl::render(const int pWindowId, /* set uniform attributes of shader * for drawing the plot borders */ - glm::mat4 model = glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(0,1,0)) * glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(1,0,0)) * glm::scale(glm::mat4(1.f), glm::vec3(1.0f, 1.0f, 1.0f)); - glm::mat4 view = glm::lookAt(glm::vec3(-1,0.5f,1.0f), glm::vec3(1,-1,-1),glm::vec3(0,1,0)); - glm::mat4 projection = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 10.f); - glm::mat4 mvp = projection * view * model; + static const glm::mat4 VIEW = glm::lookAt(glm::vec3(-1.f,0.5f, 1.f), + glm::vec3( 1.f,-1.f,-1.f), + glm::vec3( 0.f, 1.f, 0.f)); + static const glm::mat4 PROJECTION = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.f, 100.f); + static const glm::mat4 PV = PROJECTION * VIEW; + + glm::mat4 model = glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(0,1,0)) * + glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(1,0,0)) * + glm::scale(glm::mat4(1.f), glm::vec3(1.0f, 1.0f, 1.0f)); + glm::mat4 trans = PV * model; - glm::mat4 trans = mvp; glUniformMatrix4fv(mBorderUniformMatIndex, 1, GL_FALSE, glm::value_ptr(trans)); glUniform4fv(mBorderUniformColorIndex, 1, WHITE); @@ -676,11 +679,9 @@ void chart3d_impl::render(const int pWindowId, } /* render all the renderables */ - // FIXME create the correct transformation matrix - glm::mat4 transMat = glm::mat4(1); for (auto renderable : mRenderables) { renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); - renderable->render(pWindowId, pX, pY, pVPW, pVPH, transMat); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, PV); } CheckGL("End chart3d_impl::renderChart"); diff --git a/src/histogram.cpp b/src/histogram.cpp index 17d0863e..ef315446 100644 --- a/src/histogram.cpp +++ b/src/histogram.cpp @@ -28,25 +28,28 @@ namespace internal void hist_impl::bindResources(const int pWindowId) { if (mVAOMap.find(pWindowId) == mVAOMap.end()) { + CheckGL("Begin hist_impl::bindResources"); GLuint vao = 0; /* create a vertex array object * with appropriate bindings */ glGenVertexArrays(1, &vao); glBindVertexArray(vao); - glEnableVertexAttribArray(mPointIndex); - glEnableVertexAttribArray(mFreqIndex); // attach histogram bar vertices + glEnableVertexAttribArray(mPointIndex); glBindBuffer(GL_ARRAY_BUFFER, screenQuadVBO(pWindowId)); glVertexAttribPointer(mPointIndex, 2, GL_FLOAT, GL_FALSE, 0, 0); // attach histogram frequencies + glEnableVertexAttribArray(mFreqIndex); glBindBuffer(GL_ARRAY_BUFFER, mVBO); glVertexAttribPointer(mFreqIndex, 1, mGLType, GL_FALSE, 0, 0); glVertexAttribDivisor(mFreqIndex, 1); // attach histogram bar colors + glEnableVertexAttribArray(mColorIndex); glBindBuffer(GL_ARRAY_BUFFER, mCBO); glVertexAttribPointer(mColorIndex, 3, GL_FLOAT, GL_FALSE, 0, 0); glVertexAttribDivisor(mColorIndex, 1); // attach histogram bar alphas + glEnableVertexAttribArray(mAlphaIndex); glBindBuffer(GL_ARRAY_BUFFER, mABO); glVertexAttribPointer(mAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); glVertexAttribDivisor(mAlphaIndex, 1); @@ -54,6 +57,7 @@ void hist_impl::bindResources(const int pWindowId) /* store the vertex array object corresponding to * the window instance in the map */ mVAOMap[pWindowId] = vao; + CheckGL("End hist_impl::bindResources"); } glBindVertexArray(mVAOMap[pWindowId]); @@ -62,22 +66,22 @@ void hist_impl::bindResources(const int pWindowId) void hist_impl::unbindResources() const { glVertexAttribDivisor(mFreqIndex, 0); + glVertexAttribDivisor(mColorIndex, 0); + glVertexAttribDivisor(mAlphaIndex, 0); glBindVertexArray(0); } hist_impl::hist_impl(const uint pNBins, const fg::dtype pDataType) : mDataType(pDataType), mGLType(dtype2gl(mDataType)), mNBins(pNBins), - mIsPVCOn(0), mProgram(0), mYMaxIndex(-1), mNBinsIndex(-1), + mIsPVCOn(false), mProgram(0), mYMaxIndex(-1), mNBinsIndex(-1), mMatIndex(-1), mPointIndex(-1), mFreqIndex(-1), mColorIndex(-1), mAlphaIndex(-1), mPVCIndex(-1), mBColorIndex(-1) { - mColor[0] = 0.8f; - mColor[1] = 0.6f; - mColor[2] = 0.0f; - mColor[3] = 1.0f; - mLegend = std::string(""); - CheckGL("Begin hist_impl::hist_impl"); + + setColor(0.8f, 0.6f, 0.0f, 1.0f); + setLegend(std::string("")); + mProgram = initShaders(glsl::histogram_vs.c_str(), glsl::histogram_fs.c_str()); mYMaxIndex = glGetUniformLocation(mProgram, "ymax" ); @@ -85,10 +89,10 @@ hist_impl::hist_impl(const uint pNBins, const fg::dtype pDataType) mMatIndex = glGetUniformLocation(mProgram, "transform"); mPointIndex = glGetAttribLocation (mProgram, "point" ); mFreqIndex = glGetAttribLocation (mProgram, "freq" ); - mColorIndex = glGetUniformLocation(mProgram, "color" ); + mColorIndex = glGetAttribLocation (mProgram, "color" ); mAlphaIndex = glGetAttribLocation (mProgram, "alpha" ); mPVCIndex = glGetUniformLocation(mProgram, "isPVCOn" ); - mBColorIndex = glGetAttribLocation (mProgram, "barColor" ); + mBColorIndex = glGetUniformLocation(mProgram, "barColor" ); mVBOSize = mNBins; mCBOSize = 3*mVBOSize; @@ -143,12 +147,11 @@ void hist_impl::render(const int pWindowId, glUniform1f(mNBinsIndex, (GLfloat)mNBins); glUniformMatrix4fv(mMatIndex, 1, GL_FALSE, glm::value_ptr(pTransform)); glUniform1i(mPVCIndex, mIsPVCOn); - glUniform4fv(mColorIndex, 1, mColor); + glUniform4fv(mBColorIndex, 1, mColor); /* render a rectangle for each bin. Same * rectangle is scaled and translated accordingly - * for each bin. This is done by OpenGL feature of - * instanced rendering */ + * for each bin. OpenGL instanced rendering is used to do it.*/ hist_impl::bindResources(pWindowId); glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, mNBins); hist_impl::unbindResources(); diff --git a/src/histogram.hpp b/src/histogram.hpp index 33932d51..940b5b88 100644 --- a/src/histogram.hpp +++ b/src/histogram.hpp @@ -39,8 +39,6 @@ class hist_impl : public AbstractRenderable { GLuint mPVCIndex; GLuint mBColorIndex; - float mRange[6]; - std::map mVAOMap; /* bind and unbind helper functions diff --git a/src/plot.hpp b/src/plot.hpp index fc3bf569..f3d7ac68 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -53,12 +53,11 @@ class plot_impl : public AbstractRenderable { GLuint mMarkerTypeIndex; GLuint mMarkerColIndex; GLuint mMarkerMatIndex; + GLuint mMarkerRangeIndex; GLuint mMarkerPointIndex; GLuint mMarkerColorIndex; GLuint mMarkerAlphaIndex; - float mRange[6]; - std::map mVAOMap; /* bind and unbind helper functions @@ -74,7 +73,8 @@ class plot_impl : public AbstractRenderable { // attach vertices glEnableVertexAttribArray(mPlotPointIndex); glBindBuffer(GL_ARRAY_BUFFER, mVBO); - glVertexAttribPointer(mPlotPointIndex, 2, mGLType, GL_FALSE, 0, 0); + glVertexAttribPointer(mPlotPointIndex, (PLOT_TYPE==fg::FG_2D ? 2 : 3), + mGLType, GL_FALSE, 0, 0); // attach colors glEnableVertexAttribArray(mPlotColorIndex); glBindBuffer(GL_ARRAY_BUFFER, mCBO); @@ -111,29 +111,23 @@ class plot_impl : public AbstractRenderable { float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); - float coor_offset_x = ( -mRange[0] * graph_scale_x); - float coor_offset_y = ( -mRange[2] * graph_scale_y); + float coor_offset_x = (-mRange[0] * graph_scale_x); + float coor_offset_y = (-mRange[2] * graph_scale_y); if (PLOT_TYPE == fg::FG_3D) { - static const glm::mat4 VIEW = glm::lookAt(glm::vec3(-1.f,0.5f, 1.f), - glm::vec3( 1.f,-1.f,-1.f), - glm::vec3( 0.f, 1.f, 0.f)); - static const glm::mat4 PROJECTION = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 100.f); - float range_z = mRange[5] - mRange[4]; float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(range_z); float coor_offset_z = (-mRange[4] * graph_scale_z); - glm::mat4 rMat = glm::rotate(I, - -glm::radians(90.f), glm::vec3(1,0,0)); + glm::mat4 rMat = glm::rotate(I, -glm::radians(90.f), glm::vec3(1,0,0)); glm::mat4 tMat = glm::translate(I, glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)); glm::mat4 sMat = glm::scale(I, glm::vec3(1.0f * graph_scale_x, -1.0f * graph_scale_y, 1.0f * graph_scale_z)); - glm::mat4 model = rMat * tMat * sMat; + glm::mat4 model= rMat * tMat * sMat; - pOut = PROJECTION * VIEW * model; + pOut = pInput * model; } else if (PLOT_TYPE == fg::FG_2D) { //FIXME: Using hard constants for now, find a way to get chart values const float lMargin = 68; @@ -168,44 +162,42 @@ class plot_impl : public AbstractRenderable { plot_impl(const uint pNumPoints, const fg::dtype pDataType, const fg::PlotType pPlotType, const fg::MarkerType pMarkerType) : mNumPoints(pNumPoints), mDataType(pDataType), mGLType(dtype2gl(mDataType)), - mIsPVCOn(0), mMarkerType(pMarkerType), mPlotType(pPlotType), + mIsPVCOn(false), mMarkerType(pMarkerType), mPlotType(pPlotType), mPlotProgram(-1), mMarkerProgram(-1), mPlotMatIndex(-1), mPlotPVCOnIndex(-1), mPlotUColorIndex(-1), mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), mPlotAlphaIndex(-1), mMarkerPVCOnIndex(-1), mMarkerTypeIndex(-1), - mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), + mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerRangeIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) { - mColor[0] = 0.0f; - mColor[1] = 1.0f; - mColor[2] = 0.0f; - mColor[3] = 1.0f; - mLegend = std::string(""); - CheckGL("Begin plot_impl::plot_impl"); + setColor(0, 1, 0, 1); + setLegend(std::string("")); + if (PLOT_TYPE==fg::FG_2D) { - mPlotProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::histogram_fs.c_str()); - mMarkerProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::marker_fs.c_str()); + mPlotProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::histogram_fs.c_str()); + mMarkerProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::marker_fs.c_str()); + mPlotUColorIndex = glGetUniformLocation(mPlotProgram, "barColor"); } else if (PLOT_TYPE==fg::FG_3D) { - mPlotProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); - mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); + mPlotProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); + mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); + mPlotRangeIndex = glGetUniformLocation(mPlotProgram, "minmaxs"); + mMarkerRangeIndex= glGetUniformLocation(mMarkerProgram, "minmaxs"); } mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); - mPlotUColorIndex = glGetUniformLocation(mPlotProgram, "barColor"); - mPlotRangeIndex = glGetUniformLocation(mPlotProgram, "minmaxs"); - mPlotPointIndex = glGetUniformLocation(mPlotProgram, "point"); - mPlotColorIndex = glGetUniformLocation(mPlotProgram, "color"); - mPlotAlphaIndex = glGetUniformLocation(mPlotProgram, "alpha"); + mPlotPointIndex = glGetAttribLocation (mPlotProgram, "point"); + mPlotColorIndex = glGetAttribLocation (mPlotProgram, "color"); + mPlotAlphaIndex = glGetAttribLocation (mPlotProgram, "alpha"); + mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); mMarkerPVCOnIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); - mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); - mMarkerPointIndex = glGetUniformLocation(mMarkerProgram, "point"); - mMarkerColorIndex = glGetUniformLocation(mMarkerProgram, "color"); - mMarkerAlphaIndex = glGetUniformLocation(mMarkerProgram, "alpha"); + mMarkerPointIndex = glGetAttribLocation (mMarkerProgram, "point"); + mMarkerColorIndex = glGetAttribLocation (mMarkerProgram, "color"); + mMarkerAlphaIndex = glGetAttribLocation (mMarkerProgram, "alpha"); if (PLOT_TYPE==fg::FG_2D) { mVBOSize = 2*mNumPoints; @@ -256,7 +248,6 @@ class plot_impl : public AbstractRenderable { const glm::mat4& pTransform) { CheckGL("Begin plot_impl::render"); - glScissor(pX, pY, pVPW, pVPH); glEnable(GL_SCISSOR_TEST); glm::mat4 mvp(1.0); @@ -265,12 +256,14 @@ class plot_impl : public AbstractRenderable { if (mPlotType == fg::FG_LINE) { glUseProgram(mPlotProgram); - if (PLOT_TYPE== fg::FG_3D) { + if (PLOT_TYPE==fg::FG_3D) { glUniform2fv(mPlotRangeIndex, 3, mRange); } + if (PLOT_TYPE==fg::FG_2D) { + glUniform4fv(mPlotUColorIndex, 1, mColor); + } glUniformMatrix4fv(mPlotMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); glUniform1i(mPlotPVCOnIndex, mIsPVCOn); - glUniform4fv(mPlotUColorIndex, 1, mColor); plot_impl::bindResources(pWindowId); glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); @@ -282,6 +275,9 @@ class plot_impl : public AbstractRenderable { glEnable(GL_PROGRAM_POINT_SIZE); glUseProgram(mMarkerProgram); + if (PLOT_TYPE== fg::FG_3D) { + glUniform2fv(mMarkerRangeIndex, 3, mRange); + } glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); glUniform1i(mMarkerTypeIndex, mMarkerType); diff --git a/src/surface.cpp b/src/surface.cpp index 7885dfd3..b3095263 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -84,26 +84,21 @@ void surface_impl::computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput) { // identity matrix static const glm::mat4 I(1.0f); - static const glm::mat4 VIEW = glm::lookAt(glm::vec3(-1.f,0.5f, 1.f), - glm::vec3( 1.f,-1.f,-1.f), - glm::vec3( 0.f, 1.f, 0.f)); - static const glm::mat4 PROJECTION = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.1f, 100.f); float range_x = mRange[1] - mRange[0]; float range_y = mRange[3] - mRange[2]; - float range_z = mRange[5] - mRange[4]; + float range_z = mRange[5] - mRange[4]; // set scale to zero if input is constant array // otherwise compute scale factor by standard equation float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(range_z); - float coor_offset_x = ( -mRange[0] * graph_scale_x); - float coor_offset_y = ( -mRange[2] * graph_scale_y); + float coor_offset_x = (-mRange[0] * graph_scale_x); + float coor_offset_y = (-mRange[2] * graph_scale_y); float coor_offset_z = (-mRange[4] * graph_scale_z); - glm::mat4 rMat = glm::rotate(I, - -glm::radians(90.f), glm::vec3(1,0,0)); + glm::mat4 rMat = glm::rotate(I, -glm::radians(90.f), glm::vec3(1,0,0)); glm::mat4 tMat = glm::translate(I, glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)); glm::mat4 sMat = glm::scale(I, @@ -111,7 +106,7 @@ void surface_impl::computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput) glm::mat4 model = rMat * tMat * sMat; - pOut = PROJECTION * VIEW * model; + pOut = pInput * model; } void surface_impl::renderGraph(const int pWindowId, const glm::mat4& transform) @@ -160,17 +155,15 @@ surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, mSurfAlphaIndex(-1), mSurfPVCIndex(-1) { CheckGL("Begin surface_impl::surface_impl"); - mColor[0] = 0.9f; - mColor[1] = 0.5f; - mColor[2] = 0.6f; - mColor[3] = 1.0f; + setColor(0.9, 0.5, 0.6, 1.0); + setLegend(std::string("")); mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); mMarkerRangeIndex= glGetUniformLocation(mMarkerProgram, "minmaxs"); - mMarkerPointIndex= glGetUniformLocation(mMarkerProgram, "point"); - mMarkerColorIndex= glGetUniformLocation(mMarkerProgram, "color"); - mMarkerAlphaIndex= glGetUniformLocation(mMarkerProgram, "alpha"); + mMarkerPointIndex= glGetAttribLocation (mMarkerProgram, "point"); + mMarkerColorIndex= glGetAttribLocation (mMarkerProgram, "color"); + mMarkerAlphaIndex= glGetAttribLocation (mMarkerProgram, "alpha"); mMarkerPVCIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); @@ -178,10 +171,10 @@ surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, mSurfProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); mSurfMatIndex = glGetUniformLocation(mSurfProgram, "transform"); mSurfRangeIndex = glGetUniformLocation(mSurfProgram, "minmaxs"); - mSurfPointIndex = glGetUniformLocation(mSurfProgram, "point"); - mSurfColorIndex = glGetUniformLocation(mSurfProgram, "color"); - mSurfAlphaIndex = glGetUniformLocation(mSurfProgram, "alpha"); mSurfPVCIndex = glGetUniformLocation(mSurfProgram, "isPVCOn"); + mSurfPointIndex = glGetAttribLocation (mSurfProgram, "point"); + mSurfColorIndex = glGetAttribLocation (mSurfProgram, "color"); + mSurfAlphaIndex = glGetAttribLocation (mSurfProgram, "alpha"); unsigned totalPoints = mNumXPoints * mNumYPoints; diff --git a/src/surface.hpp b/src/surface.hpp index 54a1159c..27c8f7fc 100644 --- a/src/surface.hpp +++ b/src/surface.hpp @@ -49,8 +49,6 @@ class surface_impl : public AbstractRenderable { GLuint mSurfAlphaIndex; GLuint mSurfPVCIndex; - float mRange[6]; - std::map mVAOMap; /* bind and unbind helper functions From 4f8d9444ec971c039ba93f7c598d6766f4efb227 Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 20 Jan 2016 10:40:58 +0530 Subject: [PATCH 03/61] code reorganization progress update Debugging following issues: * 3d line plot and surface are not rendering anything, though the 3d charts by themselves are rendering fine. Finished: * GLSL shader to std::string headers * Ported Image class to new framework, image examples working fine * 2d line and scatter plots working fine, multiple plots per chart also working fine. * Ported histogram, and corresponding example works fine * 3d line plot and surface to new framework, 3d scatter plot is working fine. --- examples/cpu/histogram.cpp | 26 ++++++------ examples/cuda/histogram.cu | 7 ++-- src/chart.cpp | 83 +++++++++++++++++++------------------- src/common.cpp | 14 +++++++ src/common.hpp | 4 ++ src/histogram.cpp | 9 ++--- src/plot.hpp | 30 +++++++------- src/shaders/plot3_fs.glsl | 2 +- src/shaders/plot3_vs.glsl | 2 - src/surface.cpp | 13 +++--- src/surface.hpp | 1 - 11 files changed, 102 insertions(+), 89 deletions(-) diff --git a/examples/cpu/histogram.cpp b/examples/cpu/histogram.cpp index b08cee44..e4c6421b 100644 --- a/examples/cpu/histogram.cpp +++ b/examples/cpu/histogram.cpp @@ -34,7 +34,7 @@ struct Bitmap { Bitmap createBitmap(unsigned w, unsigned h); void destroyBitmap(Bitmap& bmp); void kernel(Bitmap& bmp); -void hist_freq(Bitmap& bmp, int *hist_array, const unsigned nbins); +void populateBins(Bitmap& bmp, int *hist_array, const unsigned nbins); float perlinNoise(float x, float y, float z, int tileSize); float octavesPerlin(float x, float y, float z, int octaves, float persistence, int tileSize); @@ -95,24 +95,26 @@ int main(void) { * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - int histogram_array[NBINS] = {0}; - hist_freq(bmp, &histogram_array[0], NBINS); - fg::copy(hist, histogram_array); + std::vector histArray(NBINS, 0); + populateBins(bmp, histArray.data(), NBINS); + fg::copy(hist, histArray.data()); do { + wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); + wnd.draw(1, 0, chart, "Histogram of Noisy Image"); + + wnd.swapBuffers(); + kernel(bmp); fg::copy(img, bmp.ptr); - int histogram_array[NBINS] = {0}; - hist_freq(bmp, &histogram_array[0], NBINS); + std::vector histArray(NBINS, 0); + populateBins(bmp, histArray.data(), NBINS); + // limit histogram update frequency if(fmod(t,0.4f) < 0.02f) - fg::copy(hist, histogram_array); + fg::copy(hist, histArray.data()); - wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); - wnd.draw(1, 0, chart, "Histogram of Noisy Image"); - // draw window and poll for events last - wnd.swapBuffers(); } while(!wnd.close()); return 0; @@ -149,7 +151,7 @@ void kernel(Bitmap& bmp) { tileSize++; } -void hist_freq(Bitmap& bmp, int *hist_array, const unsigned nbins){ +void populateBins(Bitmap& bmp, int *hist_array, const unsigned nbins){ for (unsigned y=0; ysetRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, trans); + } + + chart2d_impl::bindResources(pWindowId); + + /* bind the plotting shader program */ + glUseProgram(mBorderProgram); + glUniformMatrix4fv(mBorderUniformMatIndex, 1, GL_FALSE, glm::value_ptr(trans)); glUniform4fv(mBorderUniformColorIndex, 1, WHITE); - /* Draw borders */ glDrawArrays(GL_LINE_LOOP, 0, 4); @@ -370,8 +376,8 @@ void chart2d_impl::render(const int pWindowId, /* bind the sprite shader program to * draw ticks on x and y axes */ glPointSize((GLfloat)mTickSize); - glUseProgram(mSpriteProgram); + glUniform4fv(mSpriteUniformTickcolorIndex, 1, WHITE); glUniformMatrix4fv(mSpriteUniformMatIndex, 1, GL_FALSE, glm::value_ptr(trans)); /* Draw tick marks on y axis */ @@ -407,11 +413,6 @@ void chart2d_impl::render(const int pWindowId, fonter->render(pWindowId, pos, WHITE, mXTitle.c_str(), CHART2D_FONT_SIZE); } - for (auto renderable : mRenderables) { - renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); - renderable->render(pWindowId, pX, pY, pVPW, pVPH, trans); - } - CheckGL("End chart2d_impl::renderChart"); } @@ -591,45 +592,45 @@ void chart3d_impl::render(const int pWindowId, const int pX, const int pY, const int pVPW, const int pVPH, const glm::mat4& pTransform) { - CheckGL("Being chart3d_impl::renderChart"); - float w = float(pVPW - (mLeftMargin + mRightMargin + mTickSize)); - float h = float(pVPH - (mTopMargin + mBottomMargin + mTickSize)); - - chart3d_impl::bindResources(pWindowId); - - /* bind the plotting shader program */ - glUseProgram(mBorderProgram); - /* set uniform attributes of shader * for drawing the plot borders */ static const glm::mat4 VIEW = glm::lookAt(glm::vec3(-1.f,0.5f, 1.f), glm::vec3( 1.f,-1.f,-1.f), glm::vec3( 0.f, 1.f, 0.f)); static const glm::mat4 PROJECTION = glm::ortho(-2.f, 2.f, -2.f, 2.f, -1.f, 100.f); + static const glm::mat4 MODEL = glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(0,1,0)) * + glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(1,0,0)) * + glm::scale(glm::mat4(1.f), glm::vec3(1.0f, 1.0f, 1.0f)); static const glm::mat4 PV = PROJECTION * VIEW; + static const glm::mat4 PVM = PV * MODEL; - glm::mat4 model = glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(0,1,0)) * - glm::rotate(glm::mat4(1.0f), -glm::radians(90.f), glm::vec3(1,0,0)) * - glm::scale(glm::mat4(1.f), glm::vec3(1.0f, 1.0f, 1.0f)); - glm::mat4 trans = PV * model; + CheckGL("Being chart3d_impl::renderChart"); - glUniformMatrix4fv(mBorderUniformMatIndex, 1, GL_FALSE, glm::value_ptr(trans)); - glUniform4fv(mBorderUniformColorIndex, 1, WHITE); + /* render all the renderables */ + for (auto renderable : mRenderables) { + renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, PV); + } + + chart3d_impl::bindResources(pWindowId); + + glUseProgram(mBorderProgram); + glUniformMatrix4fv(mBorderUniformMatIndex, 1, GL_FALSE, glm::value_ptr(PVM)); + glUniform4fv(mBorderUniformColorIndex, 1, WHITE); /* Draw borders */ glDrawArrays(GL_LINES, 0, 6); - /* reset shader program binding */ glUseProgram(0); /* bind the sprite shader program to * draw ticks on x and y axes */ glEnable(GL_PROGRAM_POINT_SIZE); glPointSize((GLfloat)mTickSize); - glUseProgram(mSpriteProgram); + glUniform4fv(mSpriteUniformTickcolorIndex, 1, WHITE); - glUniformMatrix4fv(mSpriteUniformMatIndex, 1, GL_FALSE, glm::value_ptr(trans)); + glUniformMatrix4fv(mSpriteUniformMatIndex, 1, GL_FALSE, glm::value_ptr(PVM)); /* Draw tick marks on z axis */ glUniform1i(mSpriteUniformTickaxisIndex, 1); glDrawArrays(GL_POINTS, 6, mTickCount); @@ -643,18 +644,22 @@ void chart3d_impl::render(const int pWindowId, glUseProgram(0); glPointSize(1); glDisable(GL_PROGRAM_POINT_SIZE); + chart3d_impl::unbindResources(); - renderTickLabels(pWindowId, w, h, mZText, trans, 0); - renderTickLabels(pWindowId, w, h, mYText, trans, mTickCount); - renderTickLabels(pWindowId, w, h, mXText, trans, 2*mTickCount); + float w = float(pVPW - (mLeftMargin + mRightMargin + mTickSize)); + float h = float(pVPH - (mTopMargin + mBottomMargin + mTickSize)); + + renderTickLabels(pWindowId, w, h, mZText, PVM, 0); + renderTickLabels(pWindowId, w, h, mYText, PVM, mTickCount); + renderTickLabels(pWindowId, w, h, mXText, PVM, 2*mTickCount); auto &fonter = getChartFont(); fonter->setOthro2D(int(w), int(h)); float pos[2]; /* render chart axes titles */ if (!mZTitle.empty()) { - glm::vec4 res = trans * glm::vec4(-1.0f, -1.0f, 0.0f, 1.0f); + glm::vec4 res = PVM * glm::vec4(-1.0f, -1.0f, 0.0f, 1.0f); pos[0] = w*(res.x/res.w+1.0f)/2.0f; pos[1] = h*(res.y/res.w+1.0f)/2.0f; pos[0] -= 6*(mTickSize * (w/pVPW)); @@ -662,7 +667,7 @@ void chart3d_impl::render(const int pWindowId, fonter->render(pWindowId, pos, WHITE, mZTitle.c_str(), CHART2D_FONT_SIZE, true); } if (!mYTitle.empty()) { - glm::vec4 res = trans * glm::vec4(1.0f, 0.0f, -1.0f, 1.0f); + glm::vec4 res = PVM * glm::vec4(1.0f, 0.0f, -1.0f, 1.0f); pos[0] = w*(res.x/res.w+1.0f)/2.0f; pos[1] = h*(res.y/res.w+1.0f)/2.0f; pos[0] += 0.5 * ((mTickSize * (w/pVPW)) + mYTitle.length()/2 * CHART2D_FONT_SIZE); @@ -670,7 +675,7 @@ void chart3d_impl::render(const int pWindowId, fonter->render(pWindowId, pos, WHITE, mYTitle.c_str(), CHART2D_FONT_SIZE); } if (!mXTitle.empty()) { - glm::vec4 res = trans * glm::vec4(0.0f, -1.0f, -1.0f, 1.0f); + glm::vec4 res = PVM * glm::vec4(0.0f, -1.0f, -1.0f, 1.0f); pos[0] = w*(res.x/res.w+1.0f)/2.0f; pos[1] = h*(res.y/res.w+1.0f)/2.0f; pos[0] -= (mTickSize * (w/pVPW)) + mXTitle.length()/2 * CHART2D_FONT_SIZE; @@ -678,12 +683,6 @@ void chart3d_impl::render(const int pWindowId, fonter->render(pWindowId, pos, WHITE, mXTitle.c_str(), CHART2D_FONT_SIZE); } - /* render all the renderables */ - for (auto renderable : mRenderables) { - renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); - renderable->render(pWindowId, pX, pY, pVPW, pVPH, PV); - } - CheckGL("End chart3d_impl::renderChart"); } diff --git a/src/common.cpp b/src/common.cpp index 8da2b57e..aba4b41e 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include #include #include @@ -287,3 +289,15 @@ GLuint screenQuadVAO(const int pWindowId) return svaoMap[pWindowId]; } + +std::ostream& operator<<(std::ostream& pOut, const glm::mat4& pMat) +{ + const float* ptr = (const float*)glm::value_ptr(pMat); + pOut << "\n" << std::fixed; + pOut << ptr[0] << "\t" << ptr[1] << "\t" << ptr[2] << "\t" << ptr[3] << "\n"; + pOut << ptr[4] << "\t" << ptr[5] << "\t" << ptr[6] << "\t" << ptr[7] << "\n"; + pOut << ptr[8] << "\t" << ptr[9] << "\t" << ptr[10] << "\t" << ptr[11] << "\n"; + pOut << ptr[12] << "\t" << ptr[13] << "\t" << ptr[14] << "\t" << ptr[15] << "\n"; + pOut << "\n"; + return pOut; +} diff --git a/src/common.hpp b/src/common.hpp index 3e96befa..03d0e1ab 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -15,6 +15,7 @@ #define GLM_FORCE_RADIANS #include +#include #include @@ -126,6 +127,9 @@ GLuint screenQuadVBO(const int pWindowId); */ GLuint screenQuadVAO(const int pWindowId); +/* Print glm::mat4 to std::cout stream */ +std::ostream& operator<<(std::ostream&, const glm::mat4&); + namespace internal { diff --git a/src/histogram.cpp b/src/histogram.cpp index ef315446..24be0fd8 100644 --- a/src/histogram.cpp +++ b/src/histogram.cpp @@ -65,9 +65,6 @@ void hist_impl::bindResources(const int pWindowId) void hist_impl::unbindResources() const { - glVertexAttribDivisor(mFreqIndex, 0); - glVertexAttribDivisor(mColorIndex, 0); - glVertexAttribDivisor(mAlphaIndex, 0); glBindVertexArray(0); } @@ -87,12 +84,12 @@ hist_impl::hist_impl(const uint pNBins, const fg::dtype pDataType) mYMaxIndex = glGetUniformLocation(mProgram, "ymax" ); mNBinsIndex = glGetUniformLocation(mProgram, "nbins" ); mMatIndex = glGetUniformLocation(mProgram, "transform"); + mPVCIndex = glGetUniformLocation(mProgram, "isPVCOn" ); + mBColorIndex = glGetUniformLocation(mProgram, "barColor" ); mPointIndex = glGetAttribLocation (mProgram, "point" ); mFreqIndex = glGetAttribLocation (mProgram, "freq" ); mColorIndex = glGetAttribLocation (mProgram, "color" ); mAlphaIndex = glGetAttribLocation (mProgram, "alpha" ); - mPVCIndex = glGetUniformLocation(mProgram, "isPVCOn" ); - mBColorIndex = glGetUniformLocation(mProgram, "barColor" ); mVBOSize = mNBins; mCBOSize = 3*mVBOSize; @@ -139,8 +136,8 @@ void hist_impl::render(const int pWindowId, const glm::mat4& pTransform) { CheckGL("Begin hist_impl::render"); - glScissor(pX, pY, pVPW, pVPH); glEnable(GL_SCISSOR_TEST); + glScissor(pX, pY, pVPW, pVPH); glUseProgram(mProgram); glUniform1f(mYMaxIndex, mRange[3]); diff --git a/src/plot.hpp b/src/plot.hpp index f3d7ac68..5b4c23ba 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -53,7 +53,6 @@ class plot_impl : public AbstractRenderable { GLuint mMarkerTypeIndex; GLuint mMarkerColIndex; GLuint mMarkerMatIndex; - GLuint mMarkerRangeIndex; GLuint mMarkerPointIndex; GLuint mMarkerColorIndex; GLuint mMarkerAlphaIndex; @@ -73,8 +72,10 @@ class plot_impl : public AbstractRenderable { // attach vertices glEnableVertexAttribArray(mPlotPointIndex); glBindBuffer(GL_ARRAY_BUFFER, mVBO); - glVertexAttribPointer(mPlotPointIndex, (PLOT_TYPE==fg::FG_2D ? 2 : 3), - mGLType, GL_FALSE, 0, 0); + if (PLOT_TYPE==fg::FG_2D) + glVertexAttribPointer(mPlotPointIndex, 2, mGLType, GL_FALSE, 0, 0); + else if (PLOT_TYPE==fg::FG_3D) + glVertexAttribPointer(mPlotPointIndex, 3, mGLType, GL_FALSE, 0, 0); // attach colors glEnableVertexAttribArray(mPlotColorIndex); glBindBuffer(GL_ARRAY_BUFFER, mCBO); @@ -128,6 +129,7 @@ class plot_impl : public AbstractRenderable { glm::mat4 model= rMat * tMat * sMat; pOut = pInput * model; + glScissor(pX, pY, pVPW, pVPH); } else if (PLOT_TYPE == fg::FG_2D) { //FIXME: Using hard constants for now, find a way to get chart values const float lMargin = 68; @@ -166,7 +168,7 @@ class plot_impl : public AbstractRenderable { mPlotProgram(-1), mMarkerProgram(-1), mPlotMatIndex(-1), mPlotPVCOnIndex(-1), mPlotUColorIndex(-1), mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), mPlotAlphaIndex(-1), mMarkerPVCOnIndex(-1), mMarkerTypeIndex(-1), - mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerRangeIndex(-1), mMarkerPointIndex(-1), + mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) { CheckGL("Begin plot_impl::plot_impl"); @@ -178,12 +180,15 @@ class plot_impl : public AbstractRenderable { mPlotProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::histogram_fs.c_str()); mMarkerProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::marker_fs.c_str()); mPlotUColorIndex = glGetUniformLocation(mPlotProgram, "barColor"); + mVBOSize = 2*mNumPoints; } else if (PLOT_TYPE==fg::FG_3D) { mPlotProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); mPlotRangeIndex = glGetUniformLocation(mPlotProgram, "minmaxs"); - mMarkerRangeIndex= glGetUniformLocation(mMarkerProgram, "minmaxs"); + mVBOSize = 3*mNumPoints; } + mCBOSize = 3*mNumPoints; + mABOSize = mNumPoints; mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); @@ -199,14 +204,6 @@ class plot_impl : public AbstractRenderable { mMarkerColorIndex = glGetAttribLocation (mMarkerProgram, "color"); mMarkerAlphaIndex = glGetAttribLocation (mMarkerProgram, "alpha"); - if (PLOT_TYPE==fg::FG_2D) { - mVBOSize = 2*mNumPoints; - } else if (PLOT_TYPE==fg::FG_3D) { - mVBOSize = 3*mNumPoints; - } - mCBOSize = 3*mNumPoints; - mABOSize = mNumPoints; - #define PLOT_CREATE_BUFFERS(type) \ mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ @@ -268,16 +265,15 @@ class plot_impl : public AbstractRenderable { plot_impl::bindResources(pWindowId); glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); plot_impl::unbindResources(); + glUseProgram(0); } if (mMarkerType != fg::FG_NONE) { glEnable(GL_PROGRAM_POINT_SIZE); + glPointSize(10); glUseProgram(mMarkerProgram); - if (PLOT_TYPE== fg::FG_3D) { - glUniform2fv(mMarkerRangeIndex, 3, mRange); - } glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); glUniform1i(mMarkerTypeIndex, mMarkerType); @@ -286,8 +282,10 @@ class plot_impl : public AbstractRenderable { plot_impl::bindResources(pWindowId); glDrawArrays(GL_POINTS, 0, mNumPoints); plot_impl::unbindResources(); + glUseProgram(0); glDisable(GL_PROGRAM_POINT_SIZE); + glPointSize(1); } glDisable(GL_SCISSOR_TEST); diff --git a/src/shaders/plot3_fs.glsl b/src/shaders/plot3_fs.glsl index 91848239..150cc9c6 100644 --- a/src/shaders/plot3_fs.glsl +++ b/src/shaders/plot3_fs.glsl @@ -1,7 +1,7 @@ #version 330 -uniform bool isPVCOn; uniform vec2 minmaxs[3]; +uniform bool isPVCOn; in vec4 pervcol; in vec4 hpoint; diff --git a/src/shaders/plot3_vs.glsl b/src/shaders/plot3_vs.glsl index f7435107..5d9dcfd4 100644 --- a/src/shaders/plot3_vs.glsl +++ b/src/shaders/plot3_vs.glsl @@ -1,7 +1,6 @@ #version 330 uniform mat4 transform; -uniform vec2 minmaxs[3]; in vec3 point; in vec3 color; @@ -15,5 +14,4 @@ void main(void) hpoint = vec4(point.xyz,1); pervcol = vec4(color, alpha); gl_Position = transform * vec4(point.xyz, 1); - gl_PointSize= 10; } diff --git a/src/surface.cpp b/src/surface.cpp index b3095263..f9abbc10 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -129,7 +129,6 @@ void surface_impl::renderGraph(const int pWindowId, const glm::mat4& transform) glUseProgram(mMarkerProgram); glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); - glUniform2fv(mMarkerRangeIndex, 3, mRange); glUniform1i(mMarkerPVCIndex, mIsPVCOn); glUniform1i(mMarkerTypeIndex, mMarkerType); glUniform4fv(mMarkerColIndex, 1, mColor); @@ -160,13 +159,12 @@ surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); - mMarkerRangeIndex= glGetUniformLocation(mMarkerProgram, "minmaxs"); - mMarkerPointIndex= glGetAttribLocation (mMarkerProgram, "point"); - mMarkerColorIndex= glGetAttribLocation (mMarkerProgram, "color"); - mMarkerAlphaIndex= glGetAttribLocation (mMarkerProgram, "alpha"); mMarkerPVCIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); + mMarkerPointIndex= glGetAttribLocation (mMarkerProgram, "point"); + mMarkerColorIndex= glGetAttribLocation (mMarkerProgram, "color"); + mMarkerAlphaIndex= glGetAttribLocation (mMarkerProgram, "alpha"); mSurfProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); mSurfMatIndex = glGetUniformLocation(mSurfProgram, "transform"); @@ -212,6 +210,10 @@ surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, surface_impl::~surface_impl() { CheckGL("Begin Plot::~Plot"); + for (auto it = mVAOMap.begin(); it!=mVAOMap.end(); ++it) { + GLuint vao = it->second; + glDeleteVertexArrays(1, &vao); + } glDeleteBuffers(1, &mVBO); glDeleteBuffers(1, &mCBO); glDeleteBuffers(1, &mABO); @@ -239,7 +241,6 @@ void scatter3_impl::renderGraph(const int pWindowId, const glm::mat4& transform) glUseProgram(mMarkerProgram); glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); - glUniform2fv(mMarkerRangeIndex, 3, mRange); glUniform1i(mMarkerPVCIndex, mIsPVCOn); glUniform1i(mMarkerTypeIndex, mMarkerType); glUniform4fv(mMarkerColIndex, 1, mColor); diff --git a/src/surface.hpp b/src/surface.hpp index 27c8f7fc..da848f7f 100644 --- a/src/surface.hpp +++ b/src/surface.hpp @@ -34,7 +34,6 @@ class surface_impl : public AbstractRenderable { GLuint mSurfProgram; /* shared variable index locations */ GLuint mMarkerMatIndex; - GLuint mMarkerRangeIndex; GLuint mMarkerPointIndex; GLuint mMarkerColorIndex; GLuint mMarkerAlphaIndex; From 3587caa70c58ccf8e49b5d72108e4418faa06f65 Mon Sep 17 00:00:00 2001 From: pradeep Date: Thu, 21 Jan 2016 15:16:58 +0530 Subject: [PATCH 04/61] code reorganization finished To do: * Enable alpha blending in all renerables, shader code and required code is mostly already in place, have to add blending behavior * Add legends to the renderings * Add interactivity controls for renderables * Add event handling to control flow Finished: * Automated conversion of GLSL shader files to std::string headers * Ported all renderable objects to new framework that supports multiple renderings per Chart. * Moified examples to use modified API --- src/shaders/plot3_fs.glsl | 6 +++--- src/shaders/plot3_vs.glsl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shaders/plot3_fs.glsl b/src/shaders/plot3_fs.glsl index 150cc9c6..1382fc82 100644 --- a/src/shaders/plot3_fs.glsl +++ b/src/shaders/plot3_fs.glsl @@ -17,10 +17,10 @@ vec3 hsv2rgb(vec3 c) void main(void) { - bool nin_bounds = (hpoint.x > minmaxs[0].x || hpoint.x < minmaxs[0].y || - hpoint.y > minmaxs[1].x || hpoint.y < minmaxs[1].y || hpoint.z < minmaxs[2].y); + bool nin_bounds = (hpoint.x > minmaxs[0].y || hpoint.x < minmaxs[0].x || + hpoint.y > minmaxs[1].y || hpoint.y < minmaxs[1].x || hpoint.z < minmaxs[2].x); - float height = (minmaxs[2].x- hpoint.z)/(minmaxs[2].x-minmaxs[2].y); + float height = (minmaxs[2].y- hpoint.z)/(minmaxs[2].y-minmaxs[2].x); if(nin_bounds) discard; diff --git a/src/shaders/plot3_vs.glsl b/src/shaders/plot3_vs.glsl index 5d9dcfd4..ae497301 100644 --- a/src/shaders/plot3_vs.glsl +++ b/src/shaders/plot3_vs.glsl @@ -13,5 +13,5 @@ void main(void) { hpoint = vec4(point.xyz,1); pervcol = vec4(color, alpha); - gl_Position = transform * vec4(point.xyz, 1); + gl_Position = transform * hpoint; } From 42e95bcb527389753ea998afbd73936b8d98c625 Mon Sep 17 00:00:00 2001 From: pradeep Date: Thu, 21 Jan 2016 18:47:02 +0530 Subject: [PATCH 05/61] Added documentation for Chart class Also, updated documentation for other classes --- docs/pages/README.md | 6 ++- include/fg/chart.h | 88 +++++++++++++++++++++++++++++++++++++----- include/fg/histogram.h | 6 +-- include/fg/image.h | 4 +- include/fg/plot.h | 13 +++++-- include/fg/surface.h | 6 +-- 6 files changed, 100 insertions(+), 23 deletions(-) diff --git a/docs/pages/README.md b/docs/pages/README.md index dea7801f..c02c27a1 100644 --- a/docs/pages/README.md +++ b/docs/pages/README.md @@ -22,10 +22,12 @@ install it on your machine. We plan to provide support for alternatives to GLFW as windowing toolkit, however GLFW is the default option. Should you chose to use an alternative, you -have to chose it explicity. +have to chose it explicity while building forge. -Alternatives to GLFW which are currently under consideration are given below: +Currently supported alternatives: * [SDL2](https://www.libsdl.org/download-2.0.php) + +Alternatives to GLFW which are currently under consideration are given below: * [Qt5](https://wiki.qt.io/Qt_5) #### Email diff --git a/include/fg/chart.h b/include/fg/chart.h index d1a95058..2032b3a7 100644 --- a/include/fg/chart.h +++ b/include/fg/chart.h @@ -28,6 +28,12 @@ namespace fg /** \class Chart + + \brief Chart is base canvas where other plottable objects are rendered. + + Charts come in two types: + - \ref FG_2D - Two dimensional charts + - \ref FG_3D - Three dimensional charts */ class Chart { private: @@ -38,7 +44,7 @@ class Chart { /** Creates a Chart object with given dimensional property - \param[in] pType is chart dimension property + \param[in] cType is chart dimension property */ FGAPI Chart(const ChartType cType); @@ -50,9 +56,9 @@ class Chart { /** Set axes titles for the chart - \param[in] x is x-axis title label - \param[in] y is y-axis title label - \param[in] z is z-axis title label + \param[in] pX is x-axis title label + \param[in] pY is y-axis title label + \param[in] pZ is z-axis title label */ FGAPI void setAxesTitles(const std::string pX, const std::string pY, @@ -61,30 +67,92 @@ class Chart { /** Set axes data ranges - \param[in] xmin is x-axis minimum data value - \param[in] xmax is x-axis maximum data value - \param[in] ymin is y-axis minimum data value - \param[in] ymax is y-axis maximum data value - \param[in] zmin is z-axis minimum data value - \param[in] zmax is z-axis maximum data value + \param[in] pXmin is x-axis minimum data value + \param[in] pXmax is x-axis maximum data value + \param[in] pYmin is y-axis minimum data value + \param[in] pYmax is y-axis maximum data value + \param[in] pZmin is z-axis minimum data value + \param[in] pZmax is z-axis maximum data value */ FGAPI void setAxesLimits(const float pXmin, const float pXmax, const float pYmin, const float pYmax, const float pZmin=-1, const float pZmax=1); + /** + Add an existing Image object to the current chart + + \param[in] pImage is the Image to render on the chart + */ FGAPI void add(const Image& pImage); + + /** + Add an existing Histogram object to the current chart + + \param[in] pHistogram is the Histogram to render on the chart + */ FGAPI void add(const Histogram& pHistogram); + + /** + Add an existing Plot object to the current chart + + \param[in] pPlot is the Plot to render on the chart + */ FGAPI void add(const Plot& pPlot); + + /** + Add an existing Surface object to the current chart + + \param[in] pSurface is the Surface to render on the chart + */ FGAPI void add(const Surface& pSurface); + /** + Create and add an Image object to the current chart + + \param[in] pWidth Width of the image + \param[in] pHeight Height of the image + \param[in] pFormat Color channel format of image, uses one of the values + of \ref ChannelFormat + \param[in] pDataType takes one of the values of \ref dtype that indicates + the integral data type of histogram data + */ FGAPI Image image(const uint pWidth, const uint pHeight, const ChannelFormat pFormat=FG_RGBA, const dtype pDataType=f32); + /** + Create and add an Histogram object to the current chart + + \param[in] pNBins is number of bins the data is sorted out + \param[in] pDataType takes one of the values of \ref dtype that indicates + the integral data type of histogram data + */ FGAPI Histogram histogram(const uint pNBins, const dtype pDataType); + /** + Create and add an Plot object to the current chart + + \param[in] pNumPoints is number of data points to display + \param[in] pDataType takes one of the values of \ref dtype that indicates + the integral data type of plot data + \param[in] pPlotType dictates the type of plot/graph, + it can take one of the values of \ref PlotType + \param[in] pMarkerType indicates which symbol is rendered as marker. It can take one of + the values of \ref MarkerType. + */ FGAPI Plot plot(const uint pNumPoints, const dtype pDataType, const PlotType pPlotType=FG_LINE, const MarkerType pMarkerType=FG_NONE); + /** + Create and add an Plot object to the current chart + + \param[in] pNumXPoints is number of data points along X dimension + \param[in] pNumYPoints is number of data points along Y dimension + \param[in] pDataType takes one of the values of \ref dtype that indicates + the integral data type of plot data + \param[in] pPlotType is the render type which can be one of \ref PlotType (valid choices + are FG_SURFACE and FG_SCATTER) + \param[in] pMarkerType is the type of \ref MarkerType to draw for \ref FG_SCATTER plot type + */ FGAPI Surface surface(const uint pNumXPoints, const uint pNumYPoints, const dtype pDataType, const PlotType pPlotType=FG_SURFACE, const MarkerType pMarkerType=FG_NONE); diff --git a/include/fg/histogram.h b/include/fg/histogram.h index 70a7e22a..e9d45b48 100644 --- a/include/fg/histogram.h +++ b/include/fg/histogram.h @@ -24,7 +24,7 @@ class Window; /** \class Histogram - \brief Bar graph to display data frequencey. + \brief Histogram is a bar graph to display data frequencey. */ class Histogram { private: @@ -43,7 +43,7 @@ class Histogram { /** Copy constructor for Histogram - \param[in] other is the Histogram of which we make a copy of. + \param[in] pOther is the Histogram of which we make a copy of. */ FGAPI Histogram(const Histogram& pOther); @@ -55,7 +55,7 @@ class Histogram { /** Set the color of bar in the bar graph(histogram) - \param[in] col takes values of type fg::Color to define bar color + \param[in] pColor takes values of type fg::Color to define bar color **/ FGAPI void setColor(const Color pColor); diff --git a/include/fg/image.h b/include/fg/image.h index bebba56c..79c427da 100644 --- a/include/fg/image.h +++ b/include/fg/image.h @@ -25,6 +25,8 @@ class Window; /** \class Image + + \brief Image is plain rendering of an image over the window or sub-region of it. */ class Image { private: @@ -47,7 +49,7 @@ class Image { /** Copy constructor of Image - \param[in] other is the Image of which we make a copy of. + \param[in] pOther is the Image of which we make a copy of. */ FGAPI Image(const Image& pOther); diff --git a/include/fg/plot.h b/include/fg/plot.h index c4721ac4..5ffb3e62 100644 --- a/include/fg/plot.h +++ b/include/fg/plot.h @@ -26,7 +26,7 @@ class Window; /** \class Plot - \brief Line graph to display plots. + \brief Plot is a line graph to display two dimensional data. */ class Plot { private: @@ -39,14 +39,19 @@ class Plot { \param[in] pNumPoints is number of data points to display \param[in] pDataType takes one of the values of \ref dtype that indicates the integral data type of plot data + \param[in] pChartType dictates the dimensionality of the chart + \param[in] pPlotType dictates the type of plot/graph, + it can take one of the values of \ref PlotType + \param[in] pMarkerType indicates which symbol is rendered as marker. It can take one of + the values of \ref MarkerType. */ FGAPI Plot(const uint pNumPoints, const dtype pDataType, const ChartType pChartType, - const PlotType=FG_LINE, const MarkerType=FG_NONE); + const PlotType pPlotType=FG_LINE, const MarkerType pMarkerType=FG_NONE); /** Copy constructor for Plot - \param[in] other is the Plot of which we make a copy of. + \param[in] pOther is the Plot of which we make a copy of. */ FGAPI Plot(const Plot& pOther); @@ -58,7 +63,7 @@ class Plot { /** Set the color of line graph(plot) - \param[in] col takes values of fg::Color to define plot color + \param[in] pColor takes values of fg::Color to define plot color */ FGAPI void setColor(const fg::Color pColor); diff --git a/include/fg/surface.h b/include/fg/surface.h index 3168e4ad..2aff1d4e 100644 --- a/include/fg/surface.h +++ b/include/fg/surface.h @@ -26,7 +26,7 @@ class Window; /** \class Surface - \brief 3d graph to display plots. + \brief Surface is a graph to display three dimensional data. */ class Surface { private: @@ -50,7 +50,7 @@ class Surface { /** Copy constructor for Plot - \param[in] other is the Plot of which we make a copy of. + \param[in] pOther is the Plot of which we make a copy of. */ FGAPI Surface(const Surface& pOther); @@ -62,7 +62,7 @@ class Surface { /** Set the color of line graph(plot) - \param[in] col takes values of fg::Color to define plot color + \param[in] pColor takes values of fg::Color to define plot color */ FGAPI void setColor(const fg::Color pColor); From 697deb76a1334aaf9a97966222feaede9a941482 Mon Sep 17 00:00:00 2001 From: pradeep Date: Thu, 21 Jan 2016 20:49:13 +0530 Subject: [PATCH 06/61] Added legend rendering for charts --- examples/cpu/plotting.cpp | 18 +++++++++++------ examples/cuda/plotting.cu | 11 ++++++++-- examples/opencl/plotting.cpp | 11 ++++++++-- include/fg/chart.h | 11 ++++++++++ src/chart.cpp | 31 ++++++++++++++++++++++++---- src/chart.hpp | 9 +++++++++ src/common.hpp | 39 +++++++++++++++++++++++++----------- 7 files changed, 104 insertions(+), 26 deletions(-) diff --git a/examples/cpu/plotting.cpp b/examples/cpu/plotting.cpp index 075b8b12..05ef3ef2 100644 --- a/examples/cpu/plotting.cpp +++ b/examples/cpu/plotting.cpp @@ -67,19 +67,25 @@ int main(void) /* Create several plot objects which creates the necessary * vertex buffer objects to hold the different plot types */ - fg::Plot plt0 = chart.plot(sinData.size()/2, fg::f32); //create a default plot - fg::Plot plt1 = chart.plot(cosData.size()/2, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type - fg::Plot plt2 = chart.plot(tanData.size()/2, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape + fg::Plot plt0 = chart.plot(sinData.size()/2, fg::f32); //create a default plot + fg::Plot plt1 = chart.plot(cosData.size()/2, fg::f32, fg::FG_LINE, fg::FG_NONE); //or specify a specific plot type + fg::Plot plt2 = chart.plot(tanData.size()/2, fg::f32, fg::FG_LINE, fg::FG_TRIANGLE); //last parameter specifies marker shape fg::Plot plt3 = chart.plot(logData.size()/2, fg::f32, fg::FG_SCATTER, fg::FG_POINT); - /* * Set plot colors */ plt0.setColor(fg::FG_YELLOW); plt1.setColor(fg::FG_BLUE); - plt2.setColor(fg::FG_WHITE); //use a forge predefined color - plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + plt2.setColor(fg::FG_WHITE); //use a forge predefined color + plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + /* + * Set plot legends + */ + plt0.setLegend("Sine"); + plt1.setLegend("Cosine"); + plt2.setLegend("Tangent"); + plt3.setLegend("Log base 10"); /* copy your data into the pixel buffer object exposed by diff --git a/examples/cuda/plotting.cu b/examples/cuda/plotting.cu index 06032527..732efeeb 100644 --- a/examples/cuda/plotting.cu +++ b/examples/cuda/plotting.cu @@ -57,8 +57,15 @@ int main(void) */ plt0.setColor(fg::FG_YELLOW); plt1.setColor(fg::FG_BLUE); - plt2.setColor(fg::FG_WHITE); //use a forge predefined color - plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + plt2.setColor(fg::FG_WHITE); //use a forge predefined color + plt3.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + /* + * Set plot legends + */ + plt0.setLegend("Sine"); + plt1.setLegend("Cosine"); + plt2.setLegend("Tangent"); + plt3.setLegend("Log base 10"); CUDA_ERROR_CHECK(cudaMalloc((void**)&sin_out, sizeof(float) * DATA_SIZE * 2)); CUDA_ERROR_CHECK(cudaMalloc((void**)&cos_out, sizeof(float) * DATA_SIZE * 2)); diff --git a/examples/opencl/plotting.cpp b/examples/opencl/plotting.cpp index f7976335..3de6c20b 100644 --- a/examples/opencl/plotting.cpp +++ b/examples/opencl/plotting.cpp @@ -112,8 +112,15 @@ int main(void) */ plt0.setColor(fg::FG_BLUE); plt1.setColor(fg::FG_YELLOW); - plt2.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color - plt3.setColor(fg::FG_WHITE); //use a forge predefined color + plt2.setColor((fg::Color) 0xABFF01FF); //or any hex-valued color + plt3.setColor(fg::FG_WHITE); //use a forge predefined color + /* + * Set plot legends + */ + plt0.setLegend("Sine"); + plt1.setLegend("Cosine"); + plt2.setLegend("Tangent"); + plt3.setLegend("Log base 10"); Platform plat = getPlatform(); // Select the default platform and create a context using this platform and the GPU diff --git a/include/fg/chart.h b/include/fg/chart.h index 2032b3a7..6c532245 100644 --- a/include/fg/chart.h +++ b/include/fg/chart.h @@ -78,6 +78,17 @@ class Chart { const float pYmin, const float pYmax, const float pZmin=-1, const float pZmax=1); + /** + Set legend position for Chart + + \param[in] pX is horizontal position in normalized coordinates + \param[in] pY is vertical position in normalized coordinates + + \note By normalized coordinates, the range of these coordinates is expected to be [0-1]. + (0,0) is the bottom hand left corner. + */ + FGAPI void setLegendPosition(const float pX, const float pY); + /** Add an existing Image object to the current chart diff --git a/src/chart.cpp b/src/chart.cpp index 3daadcc5..0ba9ccf1 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -118,7 +118,8 @@ AbstractChart::AbstractChart(const int pLeftMargin, const int pRightMargin, mDecorVBO(-1), mBorderProgram(-1), mSpriteProgram(-1), mBorderAttribPointIndex(-1), mBorderUniformColorIndex(-1), mBorderUniformMatIndex(-1), mSpriteUniformMatIndex(-1), - mSpriteUniformTickcolorIndex(-1), mSpriteUniformTickaxisIndex(-1) + mSpriteUniformTickcolorIndex(-1), mSpriteUniformTickaxisIndex(-1), + mLegendX(0.4f), mLegendY(0.9f) { CheckGL("Begin AbstractChart::AbstractChart"); /* load font Vera font for chart text @@ -181,6 +182,12 @@ void AbstractChart::setAxesTitles(const std::string& pXTitle, mZTitle = std::string(pZTitle); } +void AbstractChart::setLegendPosition(const float pX, const float pY) +{ + mLegendX = pX; + mLegendY = pY; +} + float AbstractChart::xmax() const { return mXMax; } float AbstractChart::xmin() const { return mXMin; } float AbstractChart::ymax() const { return mYMax; } @@ -348,6 +355,12 @@ void chart2d_impl::render(const int pWindowId, float scale_x = w / pVPW; float scale_y = h / pVPH; + auto &fonter = getChartFont(); + fonter->setOthro2D(int(w), int(h)); + + float pos[2] = {mLegendX, mLegendY}; + float lcol[4]; + /* set uniform attributes of shader * for drawing the plot borders */ glm::mat4 trans = glm::translate(glm::scale(glm::mat4(1), @@ -358,6 +371,14 @@ void chart2d_impl::render(const int pWindowId, for (auto renderable : mRenderables) { renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); renderable->render(pWindowId, pX, pY, pVPW, pVPH, trans); + renderable->getColor(lcol[0], lcol[1], lcol[2], lcol[3]); + + float cpos[2]; + glm::vec4 res = trans * glm::vec4(pos[0], pos[1], 0.0f, 1.0f); + cpos[0] = res.x * w; + cpos[1] = res.y * h; + fonter->render(pWindowId, cpos, lcol, renderable->legend().c_str(), CHART2D_FONT_SIZE); + pos[1] -= (CHART2D_FONT_SIZE/(float)pVPH); } chart2d_impl::bindResources(pWindowId); @@ -394,9 +415,6 @@ void chart2d_impl::render(const int pWindowId, renderTickLabels(pWindowId, int(w), int(h), mYText, trans, 0, false); renderTickLabels(pWindowId, int(w), int(h), mXText, trans, mTickCount, false); - auto &fonter = getChartFont(); - fonter->setOthro2D(int(w), int(h)); - float pos[2]; /* render chart axes titles */ if (!mYTitle.empty()) { glm::vec4 res = trans * glm::vec4(-1.0f, 0.0f, 0.0f, 1.0f); @@ -716,6 +734,11 @@ void Chart::setAxesLimits(const float pXmin, const float pXmax, mValue->setAxesLimits(pXmin, pXmax, pYmin, pYmax, pZmin, pZmax); } +void Chart::setLegendPosition(const float pX, const float pY) +{ + mValue->setLegendPosition(pX, pY); +} + void Chart::add(const Image& pImage) { mValue->addRenderable(pImage.get()->impl()); diff --git a/src/chart.hpp b/src/chart.hpp index c1459ac3..1b613c04 100644 --- a/src/chart.hpp +++ b/src/chart.hpp @@ -57,6 +57,9 @@ class AbstractChart : public AbstractRenderable { GLuint mSpriteUniformMatIndex; GLuint mSpriteUniformTickcolorIndex; GLuint mSpriteUniformTickaxisIndex; + /* Chart legend position*/ + float mLegendX; + float mLegendY; /* VAO map to store a vertex array object * for each valid window context */ std::map mVAOMap; @@ -90,6 +93,8 @@ class AbstractChart : public AbstractRenderable { const float pYmin, const float pYmax, const float pZmin, const float pZmax); + void setLegendPosition(const float pX, const float pY); + float xmax() const; float xmin() const; float ymax() const; @@ -175,6 +180,10 @@ class _Chart { mChart->setAxesLimits(pXmin, pXmax, pYmin, pYmax, pZmin, pZmax); } + inline void setLegendPosition(const uint pX, const uint pY) { + mChart->setLegendPosition(pX, pY); + } + inline void addRenderable(const std::shared_ptr pRenderable) { mChart->addRenderable(pRenderable); } diff --git a/src/common.hpp b/src/common.hpp index 03d0e1ab..e98cd3a8 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -159,36 +159,51 @@ class AbstractRenderable { * cbo is for colors of those vertices * abo is for alpha values for those vertices */ - virtual GLuint vbo() const { return mVBO; } - virtual GLuint cbo() const { return mCBO; } - virtual GLuint abo() const { return mABO; } - virtual size_t vboSize() const { return mVBOSize; } - virtual size_t cboSize() const { return mCBOSize; } - virtual size_t aboSize() const { return mABOSize; } + GLuint vbo() const { return mVBO; } + GLuint cbo() const { return mCBO; } + GLuint abo() const { return mABO; } + size_t vboSize() const { return mVBOSize; } + size_t cboSize() const { return mCBOSize; } + size_t aboSize() const { return mABOSize; } /* Set color for rendering */ - virtual void setColor(const float pRed, const float pGreen, - const float pBlue, const float pAlpha) { + void setColor(const float pRed, const float pGreen, + const float pBlue, const float pAlpha) { mColor[0] = clampTo01(pRed); mColor[1] = clampTo01(pGreen); mColor[2] = clampTo01(pBlue); mColor[3] = clampTo01(pAlpha); } + /* Get renderable solid color + */ + void getColor(float& pRed, float& pGreen, float& pBlue, float& pAlpha) { + pRed = mColor[0]; + pGreen = mColor[1]; + pBlue = mColor[2]; + pAlpha = mColor[3]; + } + /* Set legend for rendering */ - virtual void setLegend(const std::string& pLegend) { + void setLegend(const std::string& pLegend) { mLegend = pLegend; } + /* Get legend string + */ + const std::string& legend() const { + return mLegend; + } + /* Set 3d world coordinate ranges * * This method is mostly used for charts and related renderables */ - virtual void setRanges(const float pMinX, const float pMaxX, - const float pMinY, const float pMaxY, - const float pMinZ, const float pMaxZ) { + void setRanges(const float pMinX, const float pMaxX, + const float pMinY, const float pMaxY, + const float pMinZ, const float pMaxZ) { mRange[0] = pMinX; mRange[1] = pMaxX; mRange[2] = pMinY; mRange[3] = pMaxY; mRange[4] = pMinZ; mRange[5] = pMaxZ; From 511737d7e29757e02c92b399282c4848b133ac5e Mon Sep 17 00:00:00 2001 From: pradeep Date: Fri, 22 Jan 2016 09:36:14 +0530 Subject: [PATCH 07/61] Removed hard coded point size from marker shader --- src/shaders/marker2d_vs.glsl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shaders/marker2d_vs.glsl b/src/shaders/marker2d_vs.glsl index 3d42fb43..cb8d8080 100644 --- a/src/shaders/marker2d_vs.glsl +++ b/src/shaders/marker2d_vs.glsl @@ -12,5 +12,4 @@ void main(void) { pervcol = vec4(color, alpha); gl_Position = transform * vec4(point.xy, 0, 1); - gl_PointSize= 10; } From 4be14d6d84148469b48041bcc0432c72b877f66b Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 25 Jan 2016 14:13:16 +0530 Subject: [PATCH 08/61] minimalistic interactivity support for windows * Translate - Left mouse click + drag * Zoom - Left mouse click + drag with ALT (either left or right) modifier * Rotate - RIght mouse click + drag Both glfw3 and SDL2 windowing toolkits support the above three interactions. TODO: * Limit transformations to the view in which the cursor is present for multiview rendering mode. * Make rotations more robust, probably quaternion based, probably virtual track ball rotation is good for plots/graphs --- src/chart.cpp | 4 +- src/glfw/window.cpp | 94 +++++++++++++++++++++++++++++++++++++++++++-- src/glfw/window.hpp | 14 +++++++ src/plot.hpp | 2 + src/sdl/window.cpp | 93 ++++++++++++++++++++++++++++++++++---------- src/sdl/window.hpp | 11 ++++++ src/window.cpp | 4 +- 7 files changed, 193 insertions(+), 29 deletions(-) diff --git a/src/chart.cpp b/src/chart.cpp index 0ba9ccf1..abd383ff 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -370,7 +370,7 @@ void chart2d_impl::render(const int pWindowId, /* render all renderables */ for (auto renderable : mRenderables) { renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); - renderable->render(pWindowId, pX, pY, pVPW, pVPH, trans); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, pTransform*trans); renderable->getColor(lcol[0], lcol[1], lcol[2], lcol[3]); float cpos[2]; @@ -627,7 +627,7 @@ void chart3d_impl::render(const int pWindowId, /* render all the renderables */ for (auto renderable : mRenderables) { renderable->setRanges(mXMin, mXMax, mYMin, mYMax, mZMin, mZMax); - renderable->render(pWindowId, pX, pY, pVPW, pVPH, PV); + renderable->render(pWindowId, pX, pY, pVPW, pVPH, PV*pTransform); } chart3d_impl::bindResources(pWindowId); diff --git a/src/glfw/window.cpp b/src/glfw/window.cpp index fdf95fc3..7a7882df 100644 --- a/src/glfw/window.cpp +++ b/src/glfw/window.cpp @@ -10,8 +10,14 @@ #include #include +#include + #include +using glm::rotate; +using glm::translate; +using glm::scale; + #define GLFW_THROW_ERROR(msg, err) \ throw fg::Error("Window constructor", __LINE__, msg, err); @@ -19,14 +25,13 @@ namespace wtk { Widget::Widget() - : mWindow(NULL), mClose(false) + : mWindow(NULL), mClose(false), mLastXPos(0), mLastYPos(0), mMVP(glm::mat4(1.0f)), mButton(-1) { } Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindow, const bool invisible) + : mClose(false), mLastXPos(0), mLastYPos(0), mMVP(glm::mat4(1.0f)), mButton(-1) { - mClose = false; - if (!glfwInit()) { std::cerr << "ERROR: GLFW wasn't able to initalize\n"; GLFW_THROW_ERROR("glfw initilization failed", fg::FG_ERR_GL_ERROR) @@ -63,13 +68,26 @@ Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindo { static_cast(glfwGetWindowUserPointer(w))->keyboardHandler(pKey, pScancode, pAction, pMods); }; - glfwSetKeyCallback(mWindow, kbCallback); auto closeCallback = [](GLFWwindow* w) { static_cast(glfwGetWindowUserPointer(w))->hide(); }; + + auto cursorCallback = [](GLFWwindow* w, double xpos, double ypos) + { + static_cast(glfwGetWindowUserPointer(w))->cursorHandler(xpos, ypos); + }; + + auto mouseButtonCallback = [](GLFWwindow* w, int button, int action, int mods) + { + static_cast(glfwGetWindowUserPointer(w))->mouseButtonHandler(button, action, mods); + }; + glfwSetWindowCloseCallback(mWindow, closeCallback); + glfwSetKeyCallback(mWindow, kbCallback); + glfwSetCursorPosCallback(mWindow, cursorCallback); + glfwSetMouseButtonCallback(mWindow, mouseButtonCallback); } Widget::~Widget() @@ -166,6 +184,74 @@ void Widget::keyboardHandler(int pKey, int pScancode, int pAction, int pMods) } } +void Widget::cursorHandler(const float pXPos, const float pYPos) +{ + static const float SPEED = 0.005f; + + float deltaX = mLastXPos - pXPos; + float deltaY = mLastYPos - pYPos; + bool majorMoveDir = abs(deltaX) > abs(deltaY); // True for Left-Right, False for Up-Down + + /** + * RIGHT + MajorMoveDir = true && deltaX > 0 => Rotate CW about Y Axis + * RIGHT + MajorMoveDir = true && deltaX < 0 => Rotate CCW about Y Axis + * RIGHT + MajorMoveDir = false && deltaY > 0 => Rotate CW about X Axis + * RIGHT + MajorMoveDir = false && deltaY > 0 => Rotate CCW about X Axis + * + * LEFT + MajorMoveDir = true => Translate by deltaX along X + * LEFT + MajorMoveDir = false => Translate by deltaY along Y + * + * (CTRL/ALT) + LEFT + MajorMoveDir = true && deltaY > 0 => Zoom In + * (CTRL/ALT) + LEFT + MajorMoveDir = true && deltaY > 0 => Zoom Out + */ + + if (mButton == GLFW_MOUSE_BUTTON_LEFT) { + // Translate + mMVP = translate(mMVP, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); + + } else if (mButton == GLFW_MOUSE_BUTTON_LEFT + 10 * GLFW_MOD_ALT || + mButton == GLFW_MOUSE_BUTTON_LEFT + 10 * GLFW_MOD_CONTROL) { + // Zoom + if(deltaY != 0) { + if(deltaY < 0) { + deltaY = 1.0 / (-deltaY); + } + mMVP = scale(mMVP, glm::vec3(pow(deltaY, SPEED))); + } + } else if (mButton == GLFW_MOUSE_BUTTON_RIGHT) { + // Rotations + if (majorMoveDir) { + // Rotate about Y axis (left <-> right) + mMVP = rotate(mMVP, (float)(SPEED * deltaX), glm::vec3(0.0, 1.0, 0.0)); + } else { + // Rotate about X axis (up <-> down)glm:: + mMVP = rotate(mMVP, (float)(SPEED * deltaY), glm::vec3(1.0, 0.0, 0.0)); + } + } + + mLastXPos = pXPos; + mLastYPos = pYPos; +} + +void Widget::mouseButtonHandler(int pButton, int pAction, int pMods) +{ + mButton = -1; + if (pButton == GLFW_MOUSE_BUTTON_LEFT && pAction == GLFW_PRESS) { + mButton = GLFW_MOUSE_BUTTON_LEFT; + } else if (pButton == GLFW_MOUSE_BUTTON_RIGHT && pAction == GLFW_PRESS) { + mButton = GLFW_MOUSE_BUTTON_RIGHT; + } else if (pButton == GLFW_MOUSE_BUTTON_MIDDLE && pAction == GLFW_PRESS) { + mButton = GLFW_MOUSE_BUTTON_MIDDLE; + } + if(pMods == GLFW_MOD_ALT || pMods == GLFW_MOD_CONTROL) { + mButton += 10 * pMods; + } + // reset UI transforms upon mouse middle click + if(pButton == GLFW_MOUSE_BUTTON_MIDDLE && pMods == GLFW_MOD_CONTROL && pAction == GLFW_PRESS) { + mMVP = glm::mat4(1.0f); + } +} + void Widget::pollEvents() { glfwPollEvents(); diff --git a/src/glfw/window.hpp b/src/glfw/window.hpp index e7f0dd98..868791df 100644 --- a/src/glfw/window.hpp +++ b/src/glfw/window.hpp @@ -24,6 +24,8 @@ #include #endif +#include + /* the short form wtk stands for * Windowing Tool Kit */ namespace wtk @@ -33,6 +35,10 @@ class Widget { private: GLFWwindow* mWindow; bool mClose; + float mLastXPos; + float mLastYPos; + glm::mat4 mMVP; + int mButton; Widget(); @@ -51,6 +57,10 @@ class Widget { void getFrameBufferSize(int* pW, int* pH); + inline const glm::mat4& getMVP() const { + return mMVP; + } + void setTitle(const char* pTitle); void setPos(int pX, int pY); @@ -69,6 +79,10 @@ class Widget { void keyboardHandler(int pKey, int pScancode, int pAction, int pMods); + void cursorHandler(float pXPos, float pYPos); + + void mouseButtonHandler(int pButton, int pAction, int pMods); + void pollEvents(); }; diff --git a/src/plot.hpp b/src/plot.hpp index 5b4c23ba..3970cf4a 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -154,6 +154,8 @@ class plot_impl : public AbstractRenderable { pOut = glm::scale(tMat, glm::vec3(graph_scale_x * view_scale_x , graph_scale_y * view_scale_y ,1)); + pOut = pInput * pOut; + glScissor(pX + lMargin + tickSize/2, pY+bMargin + tickSize/2, pVPW - lMargin - rMargin - tickSize/2, pVPH - bMargin - tMargin - tickSize/2); diff --git a/src/sdl/window.cpp b/src/sdl/window.cpp index 11787190..39d450b0 100644 --- a/src/sdl/window.cpp +++ b/src/sdl/window.cpp @@ -10,14 +10,19 @@ #include #include +#include + #ifndef OS_WIN #include #else #include #endif - #include +using glm::rotate; +using glm::translate; +using glm::scale; + #define SDL_THROW_ERROR(msg, err) \ throw fg::Error("Window constructor", __LINE__, msg, err); @@ -30,7 +35,8 @@ Widget::Widget() } Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindow, const bool invisible) - : mWindow(nullptr), mClose(false) + : mWindow(nullptr), mClose(false), + mLastXPos(0), mLastYPos(0), mMVP(glm::mat4(1.0f)), mButton(-1), mMod(-1) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { std::cerr << "ERROR: SDL wasn't able to initalize\n"; @@ -42,7 +48,7 @@ Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindo SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); + if (pWindow != nullptr) { pWindow->makeContextCurrent(); SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1); @@ -59,13 +65,17 @@ Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindo (invisible ? SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN : SDL_WINDOW_OPENGL) | SDL_WINDOW_RESIZABLE ); - mContext = SDL_GL_CreateContext(mWindow); - if (mWindow==NULL) { - std::cerr<<"Error: Could not Create SDL Window!\n"; + std::cerr<<"Error: Could not Create SDL Window!"<< SDL_GetError() << std::endl; SDL_THROW_ERROR("sdl window creation failed", fg::FG_ERR_GL_ERROR) } + mContext = SDL_GL_CreateContext(mWindow); + if (mContext==NULL) { + std::cerr<<"Error: Could not OpenGL context!" << SDL_GetError() << std::endl; + SDL_THROW_ERROR("opengl context creation failed", fg::FG_ERR_GL_ERROR) + } + SDL_GL_SetSwapInterval(1); mWindowId = SDL_GetWindowID(mWindow); } @@ -158,28 +168,69 @@ void Widget::resetCloseFlag() void Widget::pollEvents() { + static const float SPEED = 0.005f; SDL_Event evnt; SDL_PollEvent(&evnt); /* handle window events that are triggered - when 'this' window was in focus + when the window with window id 'mWindowId' is in focus */ - if (evnt.type == SDL_WINDOWEVENT && evnt.window.windowID == mWindowId) { - switch(evnt.window.event) { - case SDL_WINDOWEVENT_CLOSE: - hide(); - break; + if (evnt.key.windowID == mWindowId) { + if (evnt.type == SDL_WINDOWEVENT) { + switch(evnt.window.event) { + case SDL_WINDOWEVENT_CLOSE: mClose = true; break; + } } - } - /* handle keyboard press down events that are triggered - when 'this' window was in focus - */ - if (evnt.type == SDL_KEYDOWN && evnt.key.windowID == mWindowId) { - switch(evnt.key.keysym.sym) { - case SDLK_ESCAPE: - hide(); - break; + if (evnt.type == SDL_KEYDOWN) { + switch(evnt.key.keysym.sym) { + case SDLK_ESCAPE: mClose = true ; break; + case SDLK_LALT : mMod = SDLK_LALT; break; + case SDLK_RALT : mMod = SDLK_RALT; break; + } + } else if (evnt.type == SDL_KEYUP) { + switch(evnt.key.keysym.sym) { + case SDLK_LALT: mMod = -1; break; + case SDLK_RALT: mMod = -1; break; + } + } + + if(evnt.type == SDL_MOUSEBUTTONUP) { + if(evnt.button.button == SDL_BUTTON_MIDDLE && mMod == SDLK_LALT) { + mMVP = glm::mat4(1.0f); + } + } + + if(evnt.type == SDL_MOUSEMOTION) { + double deltaX = -evnt.motion.xrel; + double deltaY = -evnt.motion.yrel; + bool majorMoveDir = abs(deltaX) > abs(deltaY); // True for Left-Right, False for Up-Down + + if(evnt.motion.state == SDL_BUTTON_LMASK && + (mMod == SDLK_LALT || mMod == SDLK_RALT)) { + // Zoom + if(deltaY != 0) { + if(deltaY < 0) { + deltaY = 1.0 / (-deltaY); + } + mMVP = scale(mMVP, glm::vec3(pow(deltaY, SPEED))); + } + } else if (evnt.motion.state == SDL_BUTTON_LMASK) { + // Translate + mMVP = translate(mMVP, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); + } else if (evnt.motion.state == SDL_BUTTON_RMASK) { + // Rotations + if (majorMoveDir) { + // Rotate about Y axis (left <-> right) + mMVP = rotate(mMVP, (float)(SPEED * deltaX), glm::vec3(0.0, 1.0, 0.0)); + } else { + // Rotate about X axis (up <-> down)glm:: + mMVP = rotate(mMVP, (float)(SPEED * deltaY), glm::vec3(1.0, 0.0, 0.0)); + } + } + + mLastXPos = evnt.motion.x; + mLastYPos = evnt.motion.y; } } } diff --git a/src/sdl/window.hpp b/src/sdl/window.hpp index 0042359a..8e32d241 100644 --- a/src/sdl/window.hpp +++ b/src/sdl/window.hpp @@ -11,6 +11,8 @@ #include +#include + /* the short form wtk stands for * Windowing Tool Kit */ namespace wtk @@ -22,6 +24,11 @@ class Widget { SDL_GLContext mContext; bool mClose; uint32_t mWindowId; + float mLastXPos; + float mLastYPos; + glm::mat4 mMVP; + int mButton; + SDL_Keycode mMod; Widget(); @@ -40,6 +47,10 @@ class Widget { void getFrameBufferSize(int* pW, int* pH); + inline const glm::mat4& getMVP() const { + return mMVP; + } + bool getClose() const; void setTitle(const char* pTitle); diff --git a/src/window.cpp b/src/window.cpp index 34a60117..2f552a67 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -242,7 +242,7 @@ void window_impl::draw(const std::shared_ptr& pRenderable) glClearColor(GRAY[0], GRAY[1], GRAY[2], GRAY[3]); pRenderable->setColorMapUBOParams(mColorMapUBO, mUBOSize); - pRenderable->render(mID, 0, 0, wind_width, wind_height, glm::mat4(1.0f)); + pRenderable->render(mID, 0, 0, wind_width, wind_height, mWindow->getMVP()); mWindow->swapBuffers(); mWindow->pollEvents(); @@ -297,7 +297,7 @@ void window_impl::draw(int pColId, int pRowId, glClearColor(GRAY[0], GRAY[1], GRAY[2], GRAY[3]); pRenderable->setColorMapUBOParams(mColorMapUBO, mUBOSize); - pRenderable->render(mID, x_off, y_off, mCellWidth, mCellHeight, glm::mat4(1.0f)); + pRenderable->render(mID, x_off, y_off, mCellWidth, mCellHeight, mWindow->getMVP()); glDisable(GL_SCISSOR_TEST); glViewport(x_off, y_off, mCellWidth, mCellHeight); From 9817068c18eb95adb2a2f8523d7e0669d56a8621 Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 25 Jan 2016 16:21:41 +0530 Subject: [PATCH 09/61] virtual trackball rotations via interactions --- src/common.cpp | 15 +++++++++++++++ src/common.hpp | 5 +++++ src/glfw/window.cpp | 37 ++++++++++++++++--------------------- src/glfw/window.hpp | 1 + src/sdl/window.cpp | 22 +++++++++++++++------- src/sdl/window.hpp | 19 ++++++++++--------- 6 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/common.cpp b/src/common.cpp index aba4b41e..36d7ea8f 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -21,6 +21,8 @@ using namespace fg; using namespace std; +#define PI 3.14159 + typedef struct { GLuint vertex; GLuint fragment; @@ -301,3 +303,16 @@ std::ostream& operator<<(std::ostream& pOut, const glm::mat4& pMat) pOut << "\n"; return pOut; } + +glm::vec3 trackballPoint(const float pX, const float pY, + const float pWidth, const float pHeight) +{ + float d, a; + float x, y, z; + x = (2*pX - pWidth)/pWidth; + y = (pHeight - 2*pY)/pHeight; + d = sqrt(x*x+y*y); + z = cos((PI/2.0) * ((d < 1.0) ? d : 1.0)); + a = 1.0f / sqrt(x*x + y*y + z*z); + return glm::vec3(x*a,y*a,z*a); +} diff --git a/src/common.hpp b/src/common.hpp index e98cd3a8..5f7d4e4e 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -130,6 +130,11 @@ GLuint screenQuadVAO(const int pWindowId); /* Print glm::mat4 to std::cout stream */ std::ostream& operator<<(std::ostream&, const glm::mat4&); +/* get the point of the surface of track ball */ +glm::vec3 trackballPoint(const float pX, const float pY, + const float pWidth, const float pHeight); + + namespace internal { diff --git a/src/glfw/window.cpp b/src/glfw/window.cpp index 7a7882df..f62638b2 100644 --- a/src/glfw/window.cpp +++ b/src/glfw/window.cpp @@ -190,20 +190,6 @@ void Widget::cursorHandler(const float pXPos, const float pYPos) float deltaX = mLastXPos - pXPos; float deltaY = mLastYPos - pYPos; - bool majorMoveDir = abs(deltaX) > abs(deltaY); // True for Left-Right, False for Up-Down - - /** - * RIGHT + MajorMoveDir = true && deltaX > 0 => Rotate CW about Y Axis - * RIGHT + MajorMoveDir = true && deltaX < 0 => Rotate CCW about Y Axis - * RIGHT + MajorMoveDir = false && deltaY > 0 => Rotate CW about X Axis - * RIGHT + MajorMoveDir = false && deltaY > 0 => Rotate CCW about X Axis - * - * LEFT + MajorMoveDir = true => Translate by deltaX along X - * LEFT + MajorMoveDir = false => Translate by deltaY along Y - * - * (CTRL/ALT) + LEFT + MajorMoveDir = true && deltaY > 0 => Zoom In - * (CTRL/ALT) + LEFT + MajorMoveDir = true && deltaY > 0 => Zoom Out - */ if (mButton == GLFW_MOUSE_BUTTON_LEFT) { // Translate @@ -219,14 +205,23 @@ void Widget::cursorHandler(const float pXPos, const float pYPos) mMVP = scale(mMVP, glm::vec3(pow(deltaY, SPEED))); } } else if (mButton == GLFW_MOUSE_BUTTON_RIGHT) { - // Rotations - if (majorMoveDir) { - // Rotate about Y axis (left <-> right) - mMVP = rotate(mMVP, (float)(SPEED * deltaX), glm::vec3(0.0, 1.0, 0.0)); - } else { - // Rotate about X axis (up <-> down)glm:: - mMVP = rotate(mMVP, (float)(SPEED * deltaY), glm::vec3(1.0, 0.0, 0.0)); + int width, height; + glfwGetWindowSize(mWindow, &width, &height); + + glm::vec3 curPos = trackballPoint(pXPos, pYPos, width, height); + glm::vec3 delta = mLastPos - curPos; + float angle = glm::radians(90.0f * sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z)); + glm::vec3 axis( + mLastPos.y*curPos.z-mLastPos.z*curPos.y, + mLastPos.z*curPos.x-mLastPos.x*curPos.z, + mLastPos.x*curPos.y-mLastPos.y*curPos.x + ); + float dMag = sqrt(dot(delta, delta)); + float aMag = sqrt(dot(axis, axis)); + if (dMag>0 && aMag>0) { + mMVP = rotate(mMVP, angle, axis); } + mLastPos = curPos; } mLastXPos = pXPos; diff --git a/src/glfw/window.hpp b/src/glfw/window.hpp index 868791df..71e3df04 100644 --- a/src/glfw/window.hpp +++ b/src/glfw/window.hpp @@ -39,6 +39,7 @@ class Widget { float mLastYPos; glm::mat4 mMVP; int mButton; + glm::vec3 mLastPos; Widget(); diff --git a/src/sdl/window.cpp b/src/sdl/window.cpp index 39d450b0..9262b1ed 100644 --- a/src/sdl/window.cpp +++ b/src/sdl/window.cpp @@ -204,7 +204,6 @@ void Widget::pollEvents() if(evnt.type == SDL_MOUSEMOTION) { double deltaX = -evnt.motion.xrel; double deltaY = -evnt.motion.yrel; - bool majorMoveDir = abs(deltaX) > abs(deltaY); // True for Left-Right, False for Up-Down if(evnt.motion.state == SDL_BUTTON_LMASK && (mMod == SDLK_LALT || mMod == SDLK_RALT)) { @@ -220,13 +219,22 @@ void Widget::pollEvents() mMVP = translate(mMVP, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); } else if (evnt.motion.state == SDL_BUTTON_RMASK) { // Rotations - if (majorMoveDir) { - // Rotate about Y axis (left <-> right) - mMVP = rotate(mMVP, (float)(SPEED * deltaX), glm::vec3(0.0, 1.0, 0.0)); - } else { - // Rotate about X axis (up <-> down)glm:: - mMVP = rotate(mMVP, (float)(SPEED * deltaY), glm::vec3(1.0, 0.0, 0.0)); + int width, height; + SDL_GetWindowSize(mWindow, &width, &height); + glm::vec3 curPos = trackballPoint(evnt.motion.x, evnt.motion.y, width, height); + glm::vec3 delta = mLastPos - curPos; + float angle = glm::radians(90.0f * sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z)); + glm::vec3 axis( + mLastPos.y*curPos.z-mLastPos.z*curPos.y, + mLastPos.z*curPos.x-mLastPos.x*curPos.z, + mLastPos.x*curPos.y-mLastPos.y*curPos.x + ); + float dMag = sqrt(dot(delta, delta)); + float aMag = sqrt(dot(axis, axis)); + if (dMag>0 && aMag>0) { + mMVP = rotate(mMVP, angle, axis); } + mLastPos = curPos; } mLastXPos = evnt.motion.x; diff --git a/src/sdl/window.hpp b/src/sdl/window.hpp index 8e32d241..5c0e2ab7 100644 --- a/src/sdl/window.hpp +++ b/src/sdl/window.hpp @@ -20,15 +20,16 @@ namespace wtk class Widget { private: - SDL_Window* mWindow; - SDL_GLContext mContext; - bool mClose; - uint32_t mWindowId; - float mLastXPos; - float mLastYPos; - glm::mat4 mMVP; - int mButton; - SDL_Keycode mMod; + SDL_Window* mWindow; + SDL_GLContext mContext; + bool mClose; + uint32_t mWindowId; + float mLastXPos; + float mLastYPos; + glm::mat4 mMVP; + int mButton; + SDL_Keycode mMod; + glm::vec3 mLastPos; Widget(); From 5bbadd00a8801b59a0bb68786c41a805cb712917 Mon Sep 17 00:00:00 2001 From: pradeep Date: Tue, 26 Jan 2016 14:51:05 +0530 Subject: [PATCH 10/61] Limit transformations to viewport --- src/glfw/window.cpp | 61 ++++++++++++++++++++++----------- src/glfw/window.hpp | 24 +++++++++---- src/sdl/window.cpp | 42 +++++++++++++++-------- src/sdl/window.hpp | 22 ++++++++---- src/window.cpp | 82 ++++++++++++++++++++++++--------------------- src/window.hpp | 6 ---- 6 files changed, 145 insertions(+), 92 deletions(-) diff --git a/src/glfw/window.cpp b/src/glfw/window.cpp index f62638b2..2e334069 100644 --- a/src/glfw/window.cpp +++ b/src/glfw/window.cpp @@ -25,12 +25,15 @@ namespace wtk { Widget::Widget() - : mWindow(NULL), mClose(false), mLastXPos(0), mLastYPos(0), mMVP(glm::mat4(1.0f)), mButton(-1) + : mWindow(NULL), mClose(false), mLastXPos(0), mLastYPos(0), mButton(-1), + mWidth(512), mHeight(512), mRows(1), mCols(1) { + mCellWidth = mWidth; + mCellHeight = mHeight; } Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindow, const bool invisible) - : mClose(false), mLastXPos(0), mLastYPos(0), mMVP(glm::mat4(1.0f)), mButton(-1) + : mWindow(NULL), mClose(false), mLastXPos(0), mLastYPos(0), mButton(-1), mRows(1), mCols(1) { if (!glfwInit()) { std::cerr << "ERROR: GLFW wasn't able to initalize\n"; @@ -64,6 +67,11 @@ Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindo glfwSetWindowUserPointer(mWindow, this); + auto rsCallback = [](GLFWwindow* w, int pWidth, int pHeight) + { + static_cast(glfwGetWindowUserPointer(w))->resizeHandler(pWidth, pHeight); + }; + auto kbCallback = [](GLFWwindow* w, int pKey, int pScancode, int pAction, int pMods) { static_cast(glfwGetWindowUserPointer(w))->keyboardHandler(pKey, pScancode, pAction, pMods); @@ -84,10 +92,15 @@ Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindo static_cast(glfwGetWindowUserPointer(w))->mouseButtonHandler(button, action, mods); }; + glfwSetFramebufferSizeCallback(mWindow, rsCallback); glfwSetWindowCloseCallback(mWindow, closeCallback); glfwSetKeyCallback(mWindow, kbCallback); glfwSetCursorPosCallback(mWindow, cursorCallback); glfwSetMouseButtonCallback(mWindow, mouseButtonCallback); + + glfwGetFramebufferSize(mWindow, &mWidth, &mHeight); + mCellWidth = mWidth; + mCellHeight = mHeight; } Widget::~Widget() @@ -128,11 +141,6 @@ long long Widget::getDisplayHandle() #endif } -void Widget::getFrameBufferSize(int* pW, int* pH) -{ - glfwGetFramebufferSize(mWindow, pW, pH); -} - void Widget::setTitle(const char* pTitle) { glfwSetWindowTitle(mWindow, pTitle); @@ -177,6 +185,14 @@ void Widget::resetCloseFlag() } } +void Widget::resizeHandler(int pWidth, int pHeight) +{ + mWidth = pWidth; + mHeight = pHeight; + mCellWidth = mWidth / mCols; + mCellHeight = mHeight / mRows; +} + void Widget::keyboardHandler(int pKey, int pScancode, int pAction, int pMods) { if (pKey == GLFW_KEY_ESCAPE && pAction == GLFW_PRESS) { @@ -191,9 +207,13 @@ void Widget::cursorHandler(const float pXPos, const float pYPos) float deltaX = mLastXPos - pXPos; float deltaY = mLastYPos - pYPos; + int r, c; + getViewIds(&r, &c); + glm::mat4& mvp = mMVPs[r+c*mRows]; + if (mButton == GLFW_MOUSE_BUTTON_LEFT) { // Translate - mMVP = translate(mMVP, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); + mvp = translate(mvp, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); } else if (mButton == GLFW_MOUSE_BUTTON_LEFT + 10 * GLFW_MOD_ALT || mButton == GLFW_MOUSE_BUTTON_LEFT + 10 * GLFW_MOD_CONTROL) { @@ -202,7 +222,7 @@ void Widget::cursorHandler(const float pXPos, const float pYPos) if(deltaY < 0) { deltaY = 1.0 / (-deltaY); } - mMVP = scale(mMVP, glm::vec3(pow(deltaY, SPEED))); + mvp = scale(mvp, glm::vec3(pow(deltaY, SPEED))); } } else if (mButton == GLFW_MOUSE_BUTTON_RIGHT) { int width, height; @@ -219,7 +239,7 @@ void Widget::cursorHandler(const float pXPos, const float pYPos) float dMag = sqrt(dot(delta, delta)); float aMag = sqrt(dot(axis, axis)); if (dMag>0 && aMag>0) { - mMVP = rotate(mMVP, angle, axis); + mvp = rotate(mvp, angle, axis); } mLastPos = curPos; } @@ -231,19 +251,22 @@ void Widget::cursorHandler(const float pXPos, const float pYPos) void Widget::mouseButtonHandler(int pButton, int pAction, int pMods) { mButton = -1; - if (pButton == GLFW_MOUSE_BUTTON_LEFT && pAction == GLFW_PRESS) { - mButton = GLFW_MOUSE_BUTTON_LEFT; - } else if (pButton == GLFW_MOUSE_BUTTON_RIGHT && pAction == GLFW_PRESS) { - mButton = GLFW_MOUSE_BUTTON_RIGHT; - } else if (pButton == GLFW_MOUSE_BUTTON_MIDDLE && pAction == GLFW_PRESS) { - mButton = GLFW_MOUSE_BUTTON_MIDDLE; + if (pAction == GLFW_PRESS) { + switch(pButton) { + case GLFW_MOUSE_BUTTON_LEFT : mButton = GLFW_MOUSE_BUTTON_LEFT ; break; + case GLFW_MOUSE_BUTTON_RIGHT : mButton = GLFW_MOUSE_BUTTON_RIGHT ; break; + case GLFW_MOUSE_BUTTON_MIDDLE: mButton = GLFW_MOUSE_BUTTON_MIDDLE; break; + } } - if(pMods == GLFW_MOD_ALT || pMods == GLFW_MOD_CONTROL) { + if (pMods == GLFW_MOD_ALT || pMods == GLFW_MOD_CONTROL) { mButton += 10 * pMods; } // reset UI transforms upon mouse middle click - if(pButton == GLFW_MOUSE_BUTTON_MIDDLE && pMods == GLFW_MOD_CONTROL && pAction == GLFW_PRESS) { - mMVP = glm::mat4(1.0f); + if (pButton == GLFW_MOUSE_BUTTON_MIDDLE && pMods == GLFW_MOD_CONTROL && pAction == GLFW_PRESS) { + int r, c; + getViewIds(&r, &c); + glm::mat4& mvp = mMVPs[r+c*mRows]; + mvp = glm::mat4(1.0f); } } diff --git a/src/glfw/window.hpp b/src/glfw/window.hpp index 71e3df04..7f539dc8 100644 --- a/src/glfw/window.hpp +++ b/src/glfw/window.hpp @@ -37,13 +37,27 @@ class Widget { bool mClose; float mLastXPos; float mLastYPos; - glm::mat4 mMVP; int mButton; glm::vec3 mLastPos; Widget(); + inline void getViewIds(int* pRow, int* pCol) { + *pRow = mLastXPos/mCellWidth; + *pCol = mLastYPos/mCellHeight; + } + public: + /* public variables */ + int mWidth; // Framebuffer width + int mHeight; // Framebuffer height + int mRows; + int mCols; + int mCellWidth; + int mCellHeight; + std::vector mMVPs; + + /* Constructors and methods */ Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindow, const bool invisible); ~Widget(); @@ -56,12 +70,6 @@ class Widget { long long getDisplayHandle(); - void getFrameBufferSize(int* pW, int* pH); - - inline const glm::mat4& getMVP() const { - return mMVP; - } - void setTitle(const char* pTitle); void setPos(int pX, int pY); @@ -78,6 +86,8 @@ class Widget { void resetCloseFlag(); + void resizeHandler(int pWidth, int pHeight); + void keyboardHandler(int pKey, int pScancode, int pAction, int pMods); void cursorHandler(float pXPos, float pYPos); diff --git a/src/sdl/window.cpp b/src/sdl/window.cpp index 9262b1ed..31a356d1 100644 --- a/src/sdl/window.cpp +++ b/src/sdl/window.cpp @@ -30,13 +30,15 @@ namespace wtk { Widget::Widget() - : mWindow(nullptr), mClose(false) + : mWindow(nullptr), mClose(false), mLastXPos(0), mLastYPos(0), mButton(-1), + mWidth(512), mHeight(512), mRows(1), mCols(1) { + mCellWidth = mWidth; + mCellHeight = mHeight; } Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindow, const bool invisible) - : mWindow(nullptr), mClose(false), - mLastXPos(0), mLastYPos(0), mMVP(glm::mat4(1.0f)), mButton(-1), mMod(-1) + : mWindow(nullptr), mClose(false), mLastXPos(0), mLastYPos(0), mButton(-1), mRows(1), mCols(1) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { std::cerr << "ERROR: SDL wasn't able to initalize\n"; @@ -78,6 +80,9 @@ Widget::Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindo SDL_GL_SetSwapInterval(1); mWindowId = SDL_GetWindowID(mWindow); + SDL_GetWindowSize(mWindow, &mWidth, &mHeight); + mCellWidth = mWidth; + mCellHeight = mHeight; } Widget::~Widget() @@ -116,12 +121,6 @@ long long Widget::getDisplayHandle() #endif } -void Widget::getFrameBufferSize(int* pW, int* pH) -{ - /* FIXME this needs to be framebuffer size */ - SDL_GetWindowSize(mWindow, pW, pH); -} - void Widget::setTitle(const char* pTitle) { SDL_SetWindowTitle(mWindow, pTitle); @@ -178,7 +177,15 @@ void Widget::pollEvents() if (evnt.key.windowID == mWindowId) { if (evnt.type == SDL_WINDOWEVENT) { switch(evnt.window.event) { - case SDL_WINDOWEVENT_CLOSE: mClose = true; break; + case SDL_WINDOWEVENT_CLOSE: + mClose = true; + break; + case SDL_WINDOWEVENT_RESIZED: + mWidth = evnt.window.data1; + mHeight = evnt.window.data2; + mCellWidth = mWidth / mCols; + mCellHeight = mHeight / mRows; + break; } } @@ -197,7 +204,10 @@ void Widget::pollEvents() if(evnt.type == SDL_MOUSEBUTTONUP) { if(evnt.button.button == SDL_BUTTON_MIDDLE && mMod == SDLK_LALT) { - mMVP = glm::mat4(1.0f); + int r, c; + getViewIds(&r, &c); + glm::mat4& mvp = mMVPs[r+c*mRows]; + mvp = glm::mat4(1.0f); } } @@ -205,6 +215,10 @@ void Widget::pollEvents() double deltaX = -evnt.motion.xrel; double deltaY = -evnt.motion.yrel; + int r, c; + getViewIds(&r, &c); + glm::mat4& mvp = mMVPs[r+c*mRows]; + if(evnt.motion.state == SDL_BUTTON_LMASK && (mMod == SDLK_LALT || mMod == SDLK_RALT)) { // Zoom @@ -212,11 +226,11 @@ void Widget::pollEvents() if(deltaY < 0) { deltaY = 1.0 / (-deltaY); } - mMVP = scale(mMVP, glm::vec3(pow(deltaY, SPEED))); + mvp = scale(mvp, glm::vec3(pow(deltaY, SPEED))); } } else if (evnt.motion.state == SDL_BUTTON_LMASK) { // Translate - mMVP = translate(mMVP, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); + mvp = translate(mvp, glm::vec3(-deltaX, deltaY, 0.0f) * SPEED); } else if (evnt.motion.state == SDL_BUTTON_RMASK) { // Rotations int width, height; @@ -232,7 +246,7 @@ void Widget::pollEvents() float dMag = sqrt(dot(delta, delta)); float aMag = sqrt(dot(axis, axis)); if (dMag>0 && aMag>0) { - mMVP = rotate(mMVP, angle, axis); + mvp = rotate(mvp, angle, axis); } mLastPos = curPos; } diff --git a/src/sdl/window.hpp b/src/sdl/window.hpp index 5c0e2ab7..90628c3b 100644 --- a/src/sdl/window.hpp +++ b/src/sdl/window.hpp @@ -26,14 +26,28 @@ class Widget { uint32_t mWindowId; float mLastXPos; float mLastYPos; - glm::mat4 mMVP; int mButton; SDL_Keycode mMod; glm::vec3 mLastPos; Widget(); + inline void getViewIds(int* pRow, int* pCol) { + *pRow = mLastXPos/mCellWidth; + *pCol = mLastYPos/mCellHeight; + } + public: + /* public variables */ + int mWidth; // Framebuffer width + int mHeight; // Framebuffer height + int mRows; + int mCols; + int mCellWidth; + int mCellHeight; + std::vector mMVPs; + + /* Constructors and methods */ Widget(int pWidth, int pHeight, const char* pTitle, const Widget* pWindow, const bool invisible); ~Widget(); @@ -46,12 +60,6 @@ class Widget { long long getDisplayHandle(); - void getFrameBufferSize(int* pW, int* pH); - - inline const glm::mat4& getMVP() const { - return mMVP; - } - bool getClose() const; void setTitle(const char* pTitle); diff --git a/src/window.cpp b/src/window.cpp index 2f552a67..90bb63d1 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -13,6 +13,8 @@ #include #include #include + +#include #include #include @@ -50,8 +52,7 @@ void MakeContextCurrent(const window_impl* pWindow) window_impl::window_impl(int pWidth, int pHeight, const char* pTitle, std::weak_ptr pWindow, const bool invisible) - : mID(getNextUniqueId()), mWidth(pWidth), mHeight(pHeight), - mRows(0), mCols(0) + : mID(getNextUniqueId()) { if (auto observe = pWindow.lock()) { mWindow = new wtk::Widget(pWidth, pHeight, pTitle, observe->get(), invisible); @@ -110,6 +111,11 @@ window_impl::window_impl(int pWidth, int pHeight, const char* pTitle, mColorMapUBO = mCMap->defaultMap(); mUBOSize = mCMap->defaultLen(); glEnable(GL_MULTISAMPLE); + + std::vector& mats = mWindow->mMVPs; + mats.resize(mWindow->mRows*mWindow->mCols); + std::fill(mats.begin(), mats.end(), glm::mat4(1)); + CheckGL("End Window::Window"); } @@ -189,12 +195,12 @@ long long window_impl::display() const int window_impl::width() const { - return mWidth; + return mWindow->mWidth; } int window_impl::height() const { - return mHeight; + return mWindow->mHeight; } GLEWContext* window_impl::glewContext() const @@ -229,40 +235,40 @@ bool window_impl::close() void window_impl::draw(const std::shared_ptr& pRenderable) { - CheckGL("Begin draw"); + CheckGL("Begin window_impl::draw"); MakeContextCurrent(this); mWindow->resetCloseFlag(); + glViewport(0, 0, mWindow->mWidth, mWindow->mHeight); - int wind_width, wind_height; - mWindow->getFrameBufferSize(&wind_width, &wind_height); - glViewport(0, 0, wind_width, wind_height); - + const glm::mat4& mvp = mWindow->mMVPs[0]; // clear color and depth buffers glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(GRAY[0], GRAY[1], GRAY[2], GRAY[3]); + // set colormap call is equivalent to noop for non-image renderables pRenderable->setColorMapUBOParams(mColorMapUBO, mUBOSize); - pRenderable->render(mID, 0, 0, wind_width, wind_height, mWindow->getMVP()); + pRenderable->render(mID, 0, 0, mWindow->mWidth, mWindow->mHeight, mvp); mWindow->swapBuffers(); mWindow->pollEvents(); - CheckGL("End draw"); + CheckGL("End window_impl::draw"); } void window_impl::grid(int pRows, int pCols) { - mRows= pRows; - mCols= pCols; - - int wind_width, wind_height; - mWindow->getFrameBufferSize(&wind_width, &wind_height); - glViewport(0, 0, wind_width, wind_height); + glViewport(0, 0, mWindow->mWidth, mWindow->mHeight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - mCellWidth = wind_width / mCols; - mCellHeight = wind_height / mRows; -} + mWindow->mRows = pRows; + mWindow->mCols = pCols; + mWindow->mCellWidth = mWindow->mWidth / mWindow->mCols; + mWindow->mCellHeight = mWindow->mHeight / mWindow->mRows; + // resize mvp array for views to appropriate size + std::vector& mats = mWindow->mMVPs; + mats.resize(mWindow->mRows*mWindow->mCols); + std::fill(mats.begin(), mats.end(), glm::mat4(1)); +} void window_impl::draw(int pColId, int pRowId, const std::shared_ptr& pRenderable, @@ -272,44 +278,42 @@ void window_impl::draw(int pColId, int pRowId, MakeContextCurrent(this); mWindow->resetCloseFlag(); - int wind_width, wind_height; - mWindow->getFrameBufferSize(&wind_width, &wind_height); - mCellWidth = wind_width / mCols; - mCellHeight = wind_height / mRows; - float pos[2] = {0.0, 0.0}; int c = pColId; int r = pRowId; - int x_off = c * mCellWidth; - int y_off = (mRows - 1 - r) * mCellHeight; + int x_off = c * mWindow->mCellWidth; + int y_off = (mWindow->mRows - 1 - r) * mWindow->mCellHeight; + const glm::mat4& mvp = mWindow->mMVPs[r+c*mWindow->mRows]; /* following margins are tested out for various * aspect ratios and are working fine. DO NOT CHANGE. * */ - int top_margin = int(0.06f*mCellHeight); - int bot_margin = int(0.02f*mCellHeight); - int lef_margin = int(0.02f*mCellWidth); - int rig_margin = int(0.02f*mCellWidth); + int top_margin = int(0.06f*mWindow->mCellHeight); + int bot_margin = int(0.02f*mWindow->mCellHeight); + int lef_margin = int(0.02f*mWindow->mCellWidth); + int rig_margin = int(0.02f*mWindow->mCellWidth); // set viewport to render sub image - glViewport(x_off + lef_margin, y_off + bot_margin, mCellWidth - 2 * rig_margin, mCellHeight - 2 * top_margin); - glScissor(x_off + lef_margin, y_off + bot_margin, mCellWidth - 2 * rig_margin, mCellHeight - 2 * top_margin); + glViewport(x_off + lef_margin, y_off + bot_margin, + mWindow->mCellWidth - 2 * rig_margin, mWindow->mCellHeight - 2 * top_margin); + glScissor(x_off + lef_margin, y_off + bot_margin, + mWindow->mCellWidth - 2 * rig_margin, mWindow->mCellHeight - 2 * top_margin); glEnable(GL_SCISSOR_TEST); glClearColor(GRAY[0], GRAY[1], GRAY[2], GRAY[3]); + // set colormap call is equivalent to noop for non-image renderables pRenderable->setColorMapUBOParams(mColorMapUBO, mUBOSize); - pRenderable->render(mID, x_off, y_off, mCellWidth, mCellHeight, mWindow->getMVP()); + pRenderable->render(mID, x_off, y_off, mWindow->mCellWidth, mWindow->mCellHeight, mvp); glDisable(GL_SCISSOR_TEST); - glViewport(x_off, y_off, mCellWidth, mCellHeight); + glViewport(x_off, y_off, mWindow->mCellWidth, mWindow->mCellHeight); if (pTitle!=NULL) { - mFont->setOthro2D(mCellWidth, mCellHeight); - pos[0] = mCellWidth / 3.0f; - pos[1] = mCellHeight*0.92f; + mFont->setOthro2D(mWindow->mCellWidth, mWindow->mCellHeight); + pos[0] = mWindow->mCellWidth / 3.0f; + pos[1] = mWindow->mCellHeight*0.92f; mFont->render(mID, pos, RED, pTitle, 16); } - CheckGL("End draw(column, row)"); } diff --git a/src/window.hpp b/src/window.hpp index e51ee850..20335731 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -32,13 +32,7 @@ class window_impl { long long mCxt; long long mDsp; int mID; - int mWidth; - int mHeight; wtk::Widget* mWindow; - int mRows; - int mCols; - int mCellWidth; - int mCellHeight; GLEWContext* mGLEWContext; std::shared_ptr mFont; From f984fcfd7b9a9c0a6e73290fb3126edbfdb1b1bb Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 27 Jan 2016 11:31:14 +0530 Subject: [PATCH 11/61] Alpha blending for image renderable --- src/image.cpp | 38 ++++++++++++++++++++++---------------- src/image.hpp | 4 +++- src/shaders/image_fs.glsl | 16 ++++++++++++---- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/image.cpp b/src/image.cpp index fe3a8d29..bd4787ce 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -39,8 +39,9 @@ image_impl::image_impl(const uint pWidth, const uint pHeight, const fg::ChannelFormat pFormat, const fg::dtype pDataType) : mWidth(pWidth), mHeight(pHeight), mFormat(pFormat), mGLformat(ctype2gl(mFormat)), mGLiformat(ictype2gl(mFormat)), - mDataType(pDataType), mGLType(dtype2gl(mDataType)), mAlpha(1.0f), mKeepARatio(true), - mMatIndex(-1), mTexIndex(-1), mIsGrayIndex(-1), mCMapLenIndex(-1), mCMapIndex(-1) + mDataType(pDataType), mGLType(dtype2gl(mDataType)), mAlpha(1.0f), + mKeepARatio(true), mFormatSize(1), mMatIndex(-1), mTexIndex(-1), + mNumCIndex(-1), mAlphaIndex(-1), mCMapLenIndex(-1), mCMapIndex(-1) { CheckGL("Begin image_impl::image_impl"); @@ -49,7 +50,8 @@ image_impl::image_impl(const uint pWidth, const uint pHeight, mCMapIndex = glGetUniformBlockIndex(mProgram, "ColorMap"); mCMapLenIndex = glGetUniformLocation(mProgram, "cmaplen"); mTexIndex = glGetUniformLocation(mProgram, "tex"); - mIsGrayIndex = glGetUniformLocation(mProgram, "isGrayScale"); + mNumCIndex = glGetUniformLocation(mProgram, "numcomps"); + mAlphaIndex = glGetUniformLocation(mProgram, "alpha"); // Initialize OpenGL Items glGenTextures(1, &(mTex)); @@ -74,17 +76,16 @@ image_impl::image_impl(const uint pWidth, const uint pHeight, case GL_UNSIGNED_BYTE: typeSize = sizeof(uchar ); break; default: typeSize = sizeof(float); break; } - size_t formatSize = 0; switch(mFormat) { - case fg::FG_GRAYSCALE: formatSize = 1; break; - case fg::FG_RG: formatSize = 2; break; - case fg::FG_RGB: formatSize = 3; break; - case fg::FG_BGR: formatSize = 3; break; - case fg::FG_RGBA: formatSize = 4; break; - case fg::FG_BGRA: formatSize = 4; break; - default: formatSize = 1; break; + case fg::FG_GRAYSCALE: mFormatSize = 1; break; + case fg::FG_RG: mFormatSize = 2; break; + case fg::FG_RGB: mFormatSize = 3; break; + case fg::FG_BGR: mFormatSize = 3; break; + case fg::FG_RGBA: mFormatSize = 4; break; + case fg::FG_BGRA: mFormatSize = 4; break; + default: mFormatSize = 1; break; } - mPBOsize = mWidth * mHeight * formatSize * typeSize; + mPBOsize = mWidth * mHeight * mFormatSize * typeSize; glBufferData(GL_PIXEL_UNPACK_BUFFER, mPBOsize, NULL, GL_STREAM_COPY); glBindTexture(GL_TEXTURE_2D, 0); @@ -134,6 +135,8 @@ void image_impl::render(const int pWindowId, const int pX, const int pY, const int pVPW, const int pVPH, const glm::mat4& pTransform) { + CheckGL("Begin image_impl::render"); + float xscale = 1.f; float yscale = 1.f; if (mKeepARatio) { @@ -152,9 +155,12 @@ void image_impl::render(const int pWindowId, glm::mat4 strans = glm::scale(pTransform, glm::vec3(xscale, yscale, 1)); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glUseProgram(mProgram); - glUniform1i(mIsGrayIndex, mFormat==fg::FG_GRAYSCALE); + glUniform1i(mNumCIndex, mFormatSize); + glUniform1f(mAlphaIndex, mAlpha); // load texture from PBO glActiveTexture(GL_TEXTURE0); @@ -172,8 +178,6 @@ void image_impl::render(const int pWindowId, glBindBufferBase(GL_UNIFORM_BUFFER, 0, mColorMapUBO); glUniformBlockBinding(mProgram, mCMapIndex, 0); - CheckGL("Before render"); - // Draw to screen bindResources(pWindowId); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); @@ -184,7 +188,9 @@ void image_impl::render(const int pWindowId, // ubind the shader program glUseProgram(0); - CheckGL("After render"); + glDisable(GL_BLEND); + + CheckGL("End image_impl::render"); } } diff --git a/src/image.hpp b/src/image.hpp index 01024a50..483f281c 100644 --- a/src/image.hpp +++ b/src/image.hpp @@ -27,6 +27,7 @@ class image_impl : public AbstractRenderable { GLenum mGLType; float mAlpha; bool mKeepARatio; + size_t mFormatSize; /* internal resources for interop */ size_t mPBOsize; GLuint mPBO; @@ -34,7 +35,8 @@ class image_impl : public AbstractRenderable { GLuint mProgram; GLuint mMatIndex; GLuint mTexIndex; - GLuint mIsGrayIndex; + GLuint mNumCIndex; + GLuint mAlphaIndex; GLuint mCMapLenIndex; GLuint mCMapIndex; /* color map details */ diff --git a/src/shaders/image_fs.glsl b/src/shaders/image_fs.glsl index a0338bea..2397c8c1 100644 --- a/src/shaders/image_fs.glsl +++ b/src/shaders/image_fs.glsl @@ -9,7 +9,8 @@ layout(std140) uniform ColorMap uniform float cmaplen; uniform sampler2D tex; -uniform bool isGrayScale; +uniform int numcomps; +uniform float alpha; in vec2 texcoord; out vec4 fragColor; @@ -18,14 +19,21 @@ void main() { vec4 tcolor = texture(tex, texcoord); vec4 clrs = vec4(1, 0, 0, 1); - if(isGrayScale) - clrs = vec4(tcolor.r, tcolor.r, tcolor.r, 1); + if(numcomps == 1) + clrs = vec4(tcolor.r, tcolor.r, tcolor.r, alpha); + else if (numcomps==2) + clrs = vec4(tcolor.r, tcolor.g, 1, alpha); + else if (numcomps==3) + clrs = vec4(tcolor.r, tcolor.g, tcolor.b, alpha); else clrs = tcolor; + float aval = clrs.a; + vec4 fidx = (cmaplen-1) * clrs; ivec4 idx = ivec4(fidx.x, fidx.y, fidx.z, fidx.w); float r_ch = ch[idx.x].r; float g_ch = ch[idx.y].g; float b_ch = ch[idx.z].b; - fragColor = vec4(r_ch, g_ch , b_ch, 1); + + fragColor = vec4(r_ch, g_ch , b_ch, aval); } From 21a9a03e485822dea459089552c19a2bd5e4af6f Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 27 Jan 2016 21:09:43 +0530 Subject: [PATCH 12/61] alpha blending for histogram, plots & surface Modified histogram cpu, cuda & OpenCL to show case per vertex/primitive colors feature Similar examples to show case alpha blending for plots will be added soon. --- examples/cpu/histogram.cpp | 85 ++++++++++++++++-------------- examples/cuda/histogram.cu | 97 ++++++++++++++++++++++++++--------- examples/opencl/histogram.cpp | 49 +++++++++++++----- include/CPUCopy.hpp | 28 ++++++++-- include/CUDACopy.hpp | 23 ++++++++- include/OpenCLCopy.hpp | 30 +++++++++-- src/common.hpp | 6 ++- src/histogram.cpp | 13 +++-- src/histogram.hpp | 2 +- src/plot.hpp | 23 ++++++--- src/shaders/histogram_fs.glsl | 4 +- src/shaders/marker_fs.glsl | 6 ++- src/shaders/plot3_fs.glsl | 5 +- src/surface.cpp | 18 +++++-- src/surface.hpp | 11 ++++ 15 files changed, 298 insertions(+), 102 deletions(-) diff --git a/examples/cpu/histogram.cpp b/examples/cpu/histogram.cpp index e4c6421b..cc2e233c 100644 --- a/examples/cpu/histogram.cpp +++ b/examples/cpu/histogram.cpp @@ -31,18 +31,22 @@ struct Bitmap { unsigned width; unsigned height; }; + Bitmap createBitmap(unsigned w, unsigned h); + void destroyBitmap(Bitmap& bmp); + void kernel(Bitmap& bmp); -void populateBins(Bitmap& bmp, int *hist_array, const unsigned nbins); + +void populateBins(Bitmap& bmp, int *hist_array, const unsigned nbins, float *hist_cols); float perlinNoise(float x, float y, float z, int tileSize); -float octavesPerlin(float x, float y, float z, int octaves, float persistence, int tileSize); -int main(void) { +float octavesPerlin(float x, float y, float z, int octaves, float persistence, int tileSize); +int main(void) +{ Bitmap bmp = createBitmap(DIMX, DIMY); - /* * First Forge call should be a window creation call * so that necessary OpenGL context is created for any @@ -82,39 +86,31 @@ int main(void) { */ hist.setColor(fg::FG_YELLOW); - /* - * generate image, and prepare data to pass into - * Histogram's underlying vertex buffer object - */ - kernel(bmp); - fg::copy(img, bmp.ptr); - - /* copy your data into the vertex buffer object exposed by - * fg::Histogram class and then proceed to rendering. - * To help the users with copying the data from compute - * memory to display memory, Forge provides copy headers - * along with the library to help with this task - */ - std::vector histArray(NBINS, 0); - populateBins(bmp, histArray.data(), NBINS); - fg::copy(hist, histArray.data()); - do { - wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); - wnd.draw(1, 0, chart, "Histogram of Noisy Image"); - - wnd.swapBuffers(); - + /* + * generate image, and prepare data to pass into + * Histogram's underlying vertex buffer object + */ kernel(bmp); fg::copy(img, bmp.ptr); + /* copy your data into the vertex buffer object exposed by + * fg::Histogram class and then proceed to rendering. + * To help the users with copying the data from compute + * memory to display memory, Forge provides copy headers + * along with the library to help with this task + */ std::vector histArray(NBINS, 0); - populateBins(bmp, histArray.data(), NBINS); + std::vector colArray(3*NBINS, 0.0f); + populateBins(bmp, histArray.data(), NBINS, colArray.data()); - // limit histogram update frequency - if(fmod(t,0.4f) < 0.02f) - fg::copy(hist, histArray.data()); + fg::copy(hist, histArray.data()); + fg::copy(hist, colArray.data(), fg::FG_COLOR_BUFFER); + wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); + wnd.draw(1, 0, chart, "Histogram of Noisy Image"); + + wnd.swapBuffers(); } while(!wnd.close()); return 0; @@ -135,7 +131,8 @@ void destroyBitmap(Bitmap& bmp) delete[] bmp.ptr; } -void kernel(Bitmap& bmp) { +void kernel(Bitmap& bmp) +{ static unsigned tileSize=100; for (unsigned y=0; y #include +#include +#include #include #include #include @@ -22,7 +24,6 @@ const unsigned WIN_COLS = 2; static float persistance; const unsigned NBINS = 5; - const static int hperm[] = {26, 58, 229, 82, 132, 72, 144, 251, 196, 192, 127, 16, 68, 118, 104, 213, 91, 105, 203, 61, 59, 93, 136, 249, 27, 137, 141, 223, 119, 193, 155, 43, 71, 244, 170, 115, 201, 150, 165, 78, 208, 53, 90, 232, 209, 83, @@ -42,13 +43,27 @@ const static int hperm[] = {26, 58, 229, 82, 132, 72, 144, 251, 196, 192, 127, 1 }; __constant__ int perm[256]; -void kernel(unsigned char* dev_out); -void kernel_hist(unsigned char * src, int* hist_out); + +void generateNoisyImage(unsigned char* dev_out); +void generateHistogram(unsigned char * src, int* hist_out, float* hist_colors); + +curandState_t* state; + +__global__ +void setupRandomKernel(curandState *states, unsigned long long seed) +{ + unsigned tid = blockDim.x * blockIdx.x + threadIdx.x; + curand_init(seed, tid, 0, &states[tid]); +} int main(void) { int *hist_out; + float *hist_colors; unsigned char *dev_out; + + CUDA_ERROR_CHECK(cudaMalloc((void **)&state, NBINS*sizeof(curandState_t))); + setupRandomKernel <<< 1, NBINS >>> (state, 314567); cudaMemcpyToSymbol(perm, hperm, 256 * sizeof(int)); /* @@ -90,54 +105,73 @@ int main(void) */ hist.setColor(fg::FG_YELLOW); - CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, IMG_SIZE )); + printf("1\n"); + CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, IMG_SIZE)); + printf("2\n"); CUDA_ERROR_CHECK(cudaMalloc((void**)&hist_out, NBINS * sizeof(int))); - kernel(dev_out); - kernel_hist(dev_out, hist_out); + CUDA_ERROR_CHECK(cudaMalloc((void**)&hist_colors, 3*NBINS * sizeof(float))); + + generateNoisyImage(dev_out); + generateHistogram(dev_out, hist_out, hist_colors); fg::copy(img, dev_out); fg::copy(hist, hist_out); + fg::copy(hist, hist_colors, fg::FG_COLOR_BUFFER); do { wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); wnd.draw(1, 0, chart, "Histogram of Noisy Image"); wnd.swapBuffers(); - kernel(dev_out); - kernel_hist(dev_out, hist_out); + generateNoisyImage(dev_out); + generateHistogram(dev_out, hist_out, hist_colors); fg::copy(img, dev_out); // limit histogram update frequency - if(fmod(persistance, 0.5f) < 0.01) + if(fmod(persistance, 0.5f) < 0.01) { fg::copy(hist, hist_out); + fg::copy(hist, hist_colors, fg::FG_COLOR_BUFFER); + } } while(!wnd.close()); CUDA_ERROR_CHECK(cudaFree(dev_out)); + CUDA_ERROR_CHECK(cudaFree(hist_out)); + CUDA_ERROR_CHECK(cudaFree(hist_colors)); return 0; } __device__ -inline float interp(float t){ +inline float interp(float t) +{ return ((6 * t - 15) * t + 10) * t * t * t; } __device__ -inline float lerp (float x0, float x1, float t) { +inline float lerp (float x0, float x1, float t) +{ return x0 + (x1 - x0) * t; } __device__ -inline float dot (float2 v0, float2 v1) { +inline float dot (float2 v0, float2 v1) +{ return v0.x*v1.x + v0.y*v1.y; } __device__ -inline float2 sub (float2 v0, float2 v1) { +inline float2 sub (float2 v0, float2 v1) +{ return make_float2(v0.x-v1.x, v0.y-v1.y); } __device__ -float perlinNoise(float x, float y, int tileSize) { - const float2 default_gradients[] = { make_float2(1,1), make_float2(-1,1),make_float2 (1,-1), make_float2(-1,-1) }; +float perlinNoise(float x, float y, int tileSize) +{ + const float2 default_gradients[] = { + make_float2(1,1), + make_float2(-1,1), + make_float2(1,-1), + make_float2(-1,-1) + }; int x_grid = x/tileSize; int y_grid = y/tileSize; unsigned rand_id0 = perm[(x_grid+2*y_grid) % 256 ] % 4; @@ -157,10 +191,10 @@ float perlinNoise(float x, float y, int tileSize) { influence_vecs[3] = dot(sub(make_float2(x,y), make_float2(1,1)), default_gradients[rand_id3]); return lerp(lerp(influence_vecs[0], influence_vecs[1], u), lerp(influence_vecs[2], influence_vecs[3], u), v); - } __device__ -float octavesPerlin(float x, float y, int octaves, float persistence, int tileSize) { +float octavesPerlin(float x, float y, int octaves, float persistence, int tileSize) +{ float total = 0, max_value = 0; float amplitude = 1, frequency = 1; for(int i=0; i>>(dev_out, persistance, tileSize); + imageKernel<<< blocks, threads >>>(dev_out, persistance, tileSize); } __global__ -void hist_freq(const unsigned char* src, int* hist_array, const unsigned nbins) { +void histogramKernel(const unsigned char* src, int* hist_array, const unsigned nbins) +{ int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; @@ -216,11 +253,25 @@ void hist_freq(const unsigned char* src, int* hist_array, const unsigned nbins) } } -void kernel_hist(unsigned char * src, int* hist_out){ +__global__ +void histColorsKernel(float* hist_colors, curandState *states) +{ + int bin = blockIdx.x * blockDim.x + threadIdx.x; + + hist_colors[3*bin+0] = curand_uniform(&states[bin]); + hist_colors[3*bin+1] = curand_uniform(&states[bin]); + hist_colors[3*bin+2] = curand_uniform(&states[bin]); +} + +void generateHistogram(unsigned char * src, int* hist_out, float* hist_colors) +{ static const dim3 threads(8, 8); dim3 blocks(divup(DIMX, threads.x), divup(DIMY, threads.y)); cudaMemset(hist_out, 0, NBINS * sizeof(int)); - hist_freq<<< blocks, threads >>>(src, hist_out, NBINS); + + histogramKernel<<< blocks, threads >>>(src, hist_out, NBINS); + + histColorsKernel<<< 1, NBINS >>>(hist_colors, state); } diff --git a/examples/opencl/histogram.cpp b/examples/opencl/histogram.cpp index f897aaa7..b1af4947 100644 --- a/examples/opencl/histogram.cpp +++ b/examples/opencl/histogram.cpp @@ -133,24 +133,39 @@ static const std::string fractal_ocl_kernel = " if(get_global_id(0) < size) {\n" " out[get_global_id(0)] = 0;\n" " }\n" +"}\n" +"float rand(int x)\n" +"{\n" +" x = (x << 13) ^ x;\n" +" return ( 1.0 - ( (x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0);\n" +"}\n" +"kernel\n" +"void set_colors(global float* out, uint const seed)\n" +"{\n" +" int i = get_global_id(0);\n" +" out[3*i+0] = (1+rand(seed * i))/2.0f;\n" +" out[3*i+1] = (1+rand(seed ^ i))/2.0f;\n" +" out[3*i+2] = (1+rand(seed / i))/2.0f;\n" "}\n"; inline int divup(int a, int b) { return (a+b-1)/b; } -void kernel(cl::Buffer& devOut, cl::Buffer& histOut, cl::CommandQueue& queue) + +void kernel(cl::Buffer& devOut, cl::Buffer& histOut, cl::Buffer& colors, cl::CommandQueue& queue) { static std::once_flag compileFlag; static cl::Program prog; - static cl::Kernel kern_img, kern_hist, kern_zero; + static cl::Kernel kern_img, kern_hist, kern_zero, kern_colors; std::call_once(compileFlag, [queue]() { prog = cl::Program(queue.getInfo(), fractal_ocl_kernel, true); - kern_img = cl::Kernel(prog, "image_gen"); - kern_hist = cl::Kernel(prog, "hist_freq"); - kern_zero = cl::Kernel(prog, "zero_buffer"); + kern_img = cl::Kernel(prog, "image_gen"); + kern_hist = cl::Kernel(prog, "hist_freq"); + kern_zero = cl::Kernel(prog, "zero_buffer"); + kern_colors = cl::Kernel(prog, "set_colors"); }); static const NDRange local(16, 16); @@ -177,6 +192,10 @@ void kernel(cl::Buffer& devOut, cl::Buffer& histOut, cl::CommandQueue& queue) kern_hist.setArg(3, DIMY); kern_hist.setArg(4, NBINS); queue.enqueueNDRangeKernel(kern_hist, cl::NullRange, global, local); + + kern_colors.setArg(0, colors); + kern_colors.setArg(1, std::rand()); + queue.enqueueNDRangeKernel(kern_colors, cl::NullRange, global_hist); } int main(void) @@ -267,32 +286,36 @@ int main(void) cl::Buffer devOut(context, CL_MEM_READ_WRITE, IMG_SIZE); cl::Buffer histOut(context, CL_MEM_READ_WRITE, NBINS * sizeof(int)); + cl::Buffer colors(context, CL_MEM_READ_WRITE, 3 * NBINS * sizeof(float)); /* * generate image, and prepare data to pass into * Histogram's underlying vertex buffer object */ - kernel(devOut, histOut, queue); + kernel(devOut, histOut, colors, queue); /* To help the users with copying the data from compute * memory to display memory, Forge provides copy headers * along with the library to help with this task */ fg::copy(img, devOut, queue); fg::copy(hist, histOut, queue); + fg::copy(hist, colors, queue, fg::FG_COLOR_BUFFER); do { - kernel(devOut, histOut, queue); + wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); + wnd.draw(1, 0, chart, "Histogram of Noisy Image"); + + wnd.swapBuffers(); + + kernel(devOut, histOut, colors, queue); fg::copy(img, devOut, queue); // limit histogram update frequency - if(fmod(persistance, 0.4f) < 0.02f) + if (fmod(persistance, 0.4f) < 0.02f) { fg::copy(hist, histOut, queue); + fg::copy(hist, colors, queue, fg::FG_COLOR_BUFFER); + } - // draw window and poll for events last - wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); - wnd.draw(1, 0, chart, "Histogram of Noisy Image"); - - wnd.swapBuffers(); } while(!wnd.close()); }catch (fg::Error err) { std::cout << err.what() << "(" << err.err() << ")" << std::endl; diff --git a/include/CPUCopy.hpp b/include/CPUCopy.hpp index fde8f9a9..a3ae256c 100644 --- a/include/CPUCopy.hpp +++ b/include/CPUCopy.hpp @@ -13,6 +13,12 @@ namespace fg { +enum BufferType { + FG_VERTEX_BUFFER = 0, + FG_COLOR_BUFFER = 1, + FG_ALPHA_BUFFER = 2 +}; + template void copy(fg::Image& out, const T * dataPtr) { @@ -31,10 +37,26 @@ void copy(fg::Image& out, const T * dataPtr) * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill */ template -void copy(Renderable& out, const T * dataPtr) +void copy(Renderable& out, const T * dataPtr, const BufferType bufferType=FG_VERTEX_BUFFER) { - glBindBuffer(GL_ARRAY_BUFFER, out.vertices()); - glBufferSubData(GL_ARRAY_BUFFER, 0, out.verticesSize(), dataPtr); + unsigned rId = 0; + size_t size = 0; + switch(bufferType) { + case FG_VERTEX_BUFFER: + rId = out.vertices(); + size = out.verticesSize(); + break; + case FG_COLOR_BUFFER: + rId = out.colors(); + size = out.colorsSize(); + break; + case FG_ALPHA_BUFFER: + rId = out.alphas(); + size = out.alphasSize(); + break; + } + glBindBuffer(GL_ARRAY_BUFFER, rId); + glBufferSubData(GL_ARRAY_BUFFER, 0, size, dataPtr); glBindBuffer(GL_ARRAY_BUFFER, 0); } diff --git a/include/CUDACopy.hpp b/include/CUDACopy.hpp index cf0e0bb8..df318af9 100644 --- a/include/CUDACopy.hpp +++ b/include/CUDACopy.hpp @@ -26,6 +26,12 @@ static void handleCUDAError(cudaError_t err, const char *file, int line) namespace fg { +enum BufferType { + FG_VERTEX_BUFFER = 0, + FG_COLOR_BUFFER = 1, + FG_ALPHA_BUFFER = 2 +}; + template void copy(fg::Image& out, const T * devicePtr) { @@ -52,10 +58,23 @@ void copy(fg::Image& out, const T * devicePtr) * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill */ template -void copy(Renderable& out, const T * devicePtr) +void copy(Renderable& out, const T * devicePtr, const BufferType bufferType=FG_VERTEX_BUFFER) { + unsigned rId = 0; + switch(bufferType) { + case FG_VERTEX_BUFFER: + rId = out.vertices(); + break; + case FG_COLOR_BUFFER: + rId = out.colors(); + break; + case FG_ALPHA_BUFFER: + rId = out.alphas(); + break; + } + cudaGraphicsResource *cudaVBOResource; - CUDA_ERROR_CHECK(cudaGraphicsGLRegisterBuffer(&cudaVBOResource, out.vertices(), cudaGraphicsMapFlagsWriteDiscard)); + CUDA_ERROR_CHECK(cudaGraphicsGLRegisterBuffer(&cudaVBOResource, rId, cudaGraphicsMapFlagsWriteDiscard)); size_t num_bytes; T* vboDevicePtr = NULL; diff --git a/include/OpenCLCopy.hpp b/include/OpenCLCopy.hpp index 55d5228a..19f2efc4 100644 --- a/include/OpenCLCopy.hpp +++ b/include/OpenCLCopy.hpp @@ -13,6 +13,12 @@ namespace fg { +enum BufferType { + FG_VERTEX_BUFFER = 0, + FG_COLOR_BUFFER = 1, + FG_ALPHA_BUFFER = 2 +}; + static void copy(fg::Image& out, const cl::Buffer& in, const cl::CommandQueue& queue) { cl::BufferGL pboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, out.pbo(), NULL); @@ -37,16 +43,34 @@ static void copy(fg::Image& out, const cl::Buffer& in, const cl::CommandQueue& q * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill */ template -void copy(Renderable& out, const cl::Buffer& in, const cl::CommandQueue& queue) +void copy(Renderable& out, const cl::Buffer& in, const cl::CommandQueue& queue, + const BufferType bufferType=FG_VERTEX_BUFFER) { - cl::BufferGL vboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, out.vertices(), NULL); + unsigned rId = 0; + size_t size = 0; + switch(bufferType) { + case FG_VERTEX_BUFFER: + rId = out.vertices(); + size = out.verticesSize(); + break; + case FG_COLOR_BUFFER: + rId = out.colors(); + size = out.colorsSize(); + break; + case FG_ALPHA_BUFFER: + rId = out.alphas(); + size = out.alphasSize(); + break; + } + + cl::BufferGL vboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, rId, NULL); std::vector shared_objects; shared_objects.push_back(vboMapBuffer); glFinish(); queue.enqueueAcquireGLObjects(&shared_objects); - queue.enqueueCopyBuffer(in, vboMapBuffer, 0, 0, out.verticesSize(), NULL, NULL); + queue.enqueueCopyBuffer(in, vboMapBuffer, 0, 0, size, NULL, NULL); queue.finish(); queue.enqueueReleaseGLObjects(&shared_objects); } diff --git a/src/common.hpp b/src/common.hpp index 5f7d4e4e..543fb09c 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -155,6 +155,8 @@ class AbstractRenderable { GLfloat mColor[4]; GLfloat mRange[6]; std::string mLegend; + bool mIsPVCOn; + bool mIsPVAOn; public: /* Getter functions for OpenGL buffer objects @@ -165,8 +167,8 @@ class AbstractRenderable { * abo is for alpha values for those vertices */ GLuint vbo() const { return mVBO; } - GLuint cbo() const { return mCBO; } - GLuint abo() const { return mABO; } + GLuint cbo() { mIsPVCOn = true; return mCBO; } + GLuint abo() { mIsPVAOn = true; return mABO; } size_t vboSize() const { return mVBOSize; } size_t cboSize() const { return mCBOSize; } size_t aboSize() const { return mABOSize; } diff --git a/src/histogram.cpp b/src/histogram.cpp index 24be0fd8..20cc0238 100644 --- a/src/histogram.cpp +++ b/src/histogram.cpp @@ -70,11 +70,13 @@ void hist_impl::unbindResources() const hist_impl::hist_impl(const uint pNBins, const fg::dtype pDataType) : mDataType(pDataType), mGLType(dtype2gl(mDataType)), mNBins(pNBins), - mIsPVCOn(false), mProgram(0), mYMaxIndex(-1), mNBinsIndex(-1), - mMatIndex(-1), mPointIndex(-1), mFreqIndex(-1), mColorIndex(-1), - mAlphaIndex(-1), mPVCIndex(-1), mBColorIndex(-1) + mProgram(0), mYMaxIndex(-1), mNBinsIndex(-1), mMatIndex(-1), mPointIndex(-1), + mFreqIndex(-1), mColorIndex(-1), mAlphaIndex(-1), mPVCIndex(-1), mPVAIndex(-1), + mBColorIndex(-1) { CheckGL("Begin hist_impl::hist_impl"); + mIsPVCOn = false; + mIsPVAOn = false; setColor(0.8f, 0.6f, 0.0f, 1.0f); setLegend(std::string("")); @@ -85,6 +87,7 @@ hist_impl::hist_impl(const uint pNBins, const fg::dtype pDataType) mNBinsIndex = glGetUniformLocation(mProgram, "nbins" ); mMatIndex = glGetUniformLocation(mProgram, "transform"); mPVCIndex = glGetUniformLocation(mProgram, "isPVCOn" ); + mPVAIndex = glGetUniformLocation(mProgram, "isPVAOn" ); mBColorIndex = glGetUniformLocation(mProgram, "barColor" ); mPointIndex = glGetAttribLocation (mProgram, "point" ); mFreqIndex = glGetAttribLocation (mProgram, "freq" ); @@ -136,6 +139,8 @@ void hist_impl::render(const int pWindowId, const glm::mat4& pTransform) { CheckGL("Begin hist_impl::render"); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_SCISSOR_TEST); glScissor(pX, pY, pVPW, pVPH); glUseProgram(mProgram); @@ -144,6 +149,7 @@ void hist_impl::render(const int pWindowId, glUniform1f(mNBinsIndex, (GLfloat)mNBins); glUniformMatrix4fv(mMatIndex, 1, GL_FALSE, glm::value_ptr(pTransform)); glUniform1i(mPVCIndex, mIsPVCOn); + glUniform1i(mPVAIndex, mIsPVAOn); glUniform4fv(mBColorIndex, 1, mColor); /* render a rectangle for each bin. Same @@ -155,6 +161,7 @@ void hist_impl::render(const int pWindowId, glUseProgram(0); glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); CheckGL("End hist_impl::render"); } diff --git a/src/histogram.hpp b/src/histogram.hpp index 940b5b88..ef46cc68 100644 --- a/src/histogram.hpp +++ b/src/histogram.hpp @@ -23,7 +23,6 @@ class hist_impl : public AbstractRenderable { fg::dtype mDataType; GLenum mGLType; GLuint mNBins; - bool mIsPVCOn; /* OpenGL Objects */ GLuint mProgram; /* internal shader attributes for mProgram @@ -37,6 +36,7 @@ class hist_impl : public AbstractRenderable { GLuint mColorIndex; GLuint mAlphaIndex; GLuint mPVCIndex; + GLuint mPVAIndex; GLuint mBColorIndex; std::map mVAOMap; diff --git a/src/plot.hpp b/src/plot.hpp index 3970cf4a..83596373 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -34,7 +34,6 @@ class plot_impl : public AbstractRenderable { GLuint mNumPoints; fg::dtype mDataType; GLenum mGLType; - bool mIsPVCOn; fg::MarkerType mMarkerType; fg::PlotType mPlotType; /* OpenGL Objects */ @@ -43,6 +42,7 @@ class plot_impl : public AbstractRenderable { /* shaderd variable index locations */ GLuint mPlotMatIndex; GLuint mPlotPVCOnIndex; + GLuint mPlotPVAOnIndex; GLuint mPlotUColorIndex; GLuint mPlotRangeIndex; GLuint mPlotPointIndex; @@ -50,6 +50,7 @@ class plot_impl : public AbstractRenderable { GLuint mPlotAlphaIndex; GLuint mMarkerPVCOnIndex; + GLuint mMarkerPVAOnIndex; GLuint mMarkerTypeIndex; GLuint mMarkerColIndex; GLuint mMarkerMatIndex; @@ -166,14 +167,15 @@ class plot_impl : public AbstractRenderable { plot_impl(const uint pNumPoints, const fg::dtype pDataType, const fg::PlotType pPlotType, const fg::MarkerType pMarkerType) : mNumPoints(pNumPoints), mDataType(pDataType), mGLType(dtype2gl(mDataType)), - mIsPVCOn(false), mMarkerType(pMarkerType), mPlotType(pPlotType), - mPlotProgram(-1), mMarkerProgram(-1), mPlotMatIndex(-1), mPlotPVCOnIndex(-1), - mPlotUColorIndex(-1), mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), - mPlotAlphaIndex(-1), mMarkerPVCOnIndex(-1), mMarkerTypeIndex(-1), - mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), - mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) + mMarkerType(pMarkerType), mPlotType(pPlotType), mPlotProgram(-1), mMarkerProgram(-1), + mPlotMatIndex(-1), mPlotPVCOnIndex(-1), mPlotPVAOnIndex(-1), mPlotUColorIndex(-1), + mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), mPlotAlphaIndex(-1), + mMarkerPVCOnIndex(-1), mMarkerPVAOnIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), + mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) { CheckGL("Begin plot_impl::plot_impl"); + mIsPVCOn = false; + mIsPVAOn = false; setColor(0, 1, 0, 1); setLegend(std::string("")); @@ -194,12 +196,14 @@ class plot_impl : public AbstractRenderable { mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); + mPlotPVAOnIndex = glGetUniformLocation(mPlotProgram, "isPVAOn"); mPlotPointIndex = glGetAttribLocation (mPlotProgram, "point"); mPlotColorIndex = glGetAttribLocation (mPlotProgram, "color"); mPlotAlphaIndex = glGetAttribLocation (mPlotProgram, "alpha"); mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); mMarkerPVCOnIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); + mMarkerPVAOnIndex = glGetUniformLocation(mMarkerProgram, "isPVAOn"); mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); mMarkerPointIndex = glGetAttribLocation (mMarkerProgram, "point"); @@ -247,6 +251,8 @@ class plot_impl : public AbstractRenderable { const glm::mat4& pTransform) { CheckGL("Begin plot_impl::render"); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_SCISSOR_TEST); glm::mat4 mvp(1.0); @@ -263,6 +269,7 @@ class plot_impl : public AbstractRenderable { } glUniformMatrix4fv(mPlotMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); glUniform1i(mPlotPVCOnIndex, mIsPVCOn); + glUniform1i(mPlotPVAOnIndex, mIsPVAOn); plot_impl::bindResources(pWindowId); glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); @@ -278,6 +285,7 @@ class plot_impl : public AbstractRenderable { glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); + glUniform1i(mMarkerPVAOnIndex, mIsPVAOn); glUniform1i(mMarkerTypeIndex, mMarkerType); glUniform4fv(mMarkerColIndex, 1, mColor); @@ -291,6 +299,7 @@ class plot_impl : public AbstractRenderable { } glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); CheckGL("End plot_impl::render"); } }; diff --git a/src/shaders/histogram_fs.glsl b/src/shaders/histogram_fs.glsl index 01192174..8fb7f892 100644 --- a/src/shaders/histogram_fs.glsl +++ b/src/shaders/histogram_fs.glsl @@ -1,6 +1,7 @@ #version 330 uniform bool isPVCOn; +uniform bool isPVAOn; uniform vec4 barColor; in vec4 pervcol; @@ -8,5 +9,6 @@ out vec4 outColor; void main(void) { - outColor = isPVCOn ? pervcol : barColor; + float a = isPVAOn ? pervcol.w : 1.0; + outColor = isPVCOn ? vec4(pervcol.xyz, a) : barColor; } diff --git a/src/shaders/marker_fs.glsl b/src/shaders/marker_fs.glsl index d0a95875..7aad8f7a 100644 --- a/src/shaders/marker_fs.glsl +++ b/src/shaders/marker_fs.glsl @@ -1,6 +1,7 @@ #version 330 uniform bool isPVCOn; +uniform bool isPVAOn; uniform int marker_type; uniform vec4 marker_color; @@ -42,8 +43,11 @@ void main(void) default: in_bounds = true; } + + float a = isPVAOn ? pervcol.w : 1.0; + if(!in_bounds) discard; else - outColor = isPVCOn ? pervcol : marker_color; + outColor = isPVCOn ? vec4(pervcol.xyz, a) : marker_color; } diff --git a/src/shaders/plot3_fs.glsl b/src/shaders/plot3_fs.glsl index 1382fc82..b2918a2b 100644 --- a/src/shaders/plot3_fs.glsl +++ b/src/shaders/plot3_fs.glsl @@ -2,6 +2,7 @@ uniform vec2 minmaxs[3]; uniform bool isPVCOn; +uniform bool isPVAOn; in vec4 pervcol; in vec4 hpoint; @@ -22,8 +23,10 @@ void main(void) float height = (minmaxs[2].y- hpoint.z)/(minmaxs[2].y-minmaxs[2].x); + float a = isPVAOn ? pervcol.w : 1.0; + if(nin_bounds) discard; else - outColor = isPVCOn ? pervcol : vec4(hsv2rgb(vec3(height, 1.f, 1.f)),1); + outColor = isPVCOn ? vec4(pervcol.xyz, a) : vec4(hsv2rgb(vec3(height, 1, 1)),1); } diff --git a/src/surface.cpp b/src/surface.cpp index f9abbc10..0d21be3a 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -118,6 +118,7 @@ void surface_impl::renderGraph(const int pWindowId, const glm::mat4& transform) glUniformMatrix4fv(mSurfMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); glUniform2fv(mSurfRangeIndex, 3, mRange); glUniform1i(mSurfPVCIndex, mIsPVCOn); + glUniform1i(mSurfPVAIndex, mIsPVAOn); bindResources(pWindowId); glDrawElements(GL_TRIANGLE_STRIP, mIBOSize, GL_UNSIGNED_SHORT, (void*)0 ); @@ -130,6 +131,7 @@ void surface_impl::renderGraph(const int pWindowId, const glm::mat4& transform) glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); glUniform1i(mMarkerPVCIndex, mIsPVCOn); + glUniform1i(mMarkerPVAIndex, mIsPVAOn); glUniform1i(mMarkerTypeIndex, mMarkerType); glUniform4fv(mMarkerColIndex, 1, mColor); @@ -147,19 +149,22 @@ void surface_impl::renderGraph(const int pWindowId, const glm::mat4& transform) surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, fg::dtype pDataType, fg::MarkerType pMarkerType) : mNumXPoints(pNumXPoints),mNumYPoints(pNumYPoints), mDataType(dtype2gl(pDataType)), - mIsPVCOn(false), mMarkerType(pMarkerType), mIBO(0), mIBOSize(0), mMarkerProgram(-1), - mSurfProgram(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), - mMarkerAlphaIndex(-1), mMarkerPVCIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), + mMarkerType(pMarkerType), mIBO(0), mIBOSize(0), mMarkerProgram(-1), mSurfProgram(-1), + mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1), + mMarkerPVCIndex(-1), mMarkerPVAIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), mSurfMatIndex(-1), mSurfRangeIndex(-1), mSurfPointIndex(-1), mSurfColorIndex(-1), - mSurfAlphaIndex(-1), mSurfPVCIndex(-1) + mSurfAlphaIndex(-1), mSurfPVCIndex(-1), mSurfPVAIndex(-1) { CheckGL("Begin surface_impl::surface_impl"); + mIsPVCOn = false; + mIsPVAOn = false; setColor(0.9, 0.5, 0.6, 1.0); setLegend(std::string("")); mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); mMarkerPVCIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); + mMarkerPVAIndex = glGetUniformLocation(mMarkerProgram, "isPVAOn"); mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); mMarkerPointIndex= glGetAttribLocation (mMarkerProgram, "point"); @@ -170,6 +175,7 @@ surface_impl::surface_impl(unsigned pNumXPoints, unsigned pNumYPoints, mSurfMatIndex = glGetUniformLocation(mSurfProgram, "transform"); mSurfRangeIndex = glGetUniformLocation(mSurfProgram, "minmaxs"); mSurfPVCIndex = glGetUniformLocation(mSurfProgram, "isPVCOn"); + mSurfPVAIndex = glGetUniformLocation(mSurfProgram, "isPVAOn"); mSurfPointIndex = glGetAttribLocation (mSurfProgram, "point"); mSurfColorIndex = glGetAttribLocation (mSurfProgram, "color"); mSurfAlphaIndex = glGetAttribLocation (mSurfProgram, "alpha"); @@ -228,9 +234,12 @@ void surface_impl::render(const int pWindowId, const glm::mat4 &pModel) { CheckGL("Begin surface_impl::render"); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glm::mat4 mvp(1.0); computeTransformMat(mvp, pModel); renderGraph(pWindowId, mvp); + glDisable(GL_BLEND); CheckGL("End surface_impl::render"); } @@ -242,6 +251,7 @@ void scatter3_impl::renderGraph(const int pWindowId, const glm::mat4& transform) glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(transform)); glUniform1i(mMarkerPVCIndex, mIsPVCOn); + glUniform1i(mMarkerPVAIndex, mIsPVAOn); glUniform1i(mMarkerTypeIndex, mMarkerType); glUniform4fv(mMarkerColIndex, 1, mColor); diff --git a/src/surface.hpp b/src/surface.hpp index da848f7f..c6ad5515 100644 --- a/src/surface.hpp +++ b/src/surface.hpp @@ -26,6 +26,7 @@ class surface_impl : public AbstractRenderable { GLuint mNumYPoints; GLenum mDataType; bool mIsPVCOn; + bool mIsPVAOn; fg::MarkerType mMarkerType; /* OpenGL Objects */ GLuint mIBO; @@ -38,6 +39,7 @@ class surface_impl : public AbstractRenderable { GLuint mMarkerColorIndex; GLuint mMarkerAlphaIndex; GLuint mMarkerPVCIndex; + GLuint mMarkerPVAIndex; GLuint mMarkerTypeIndex; GLuint mMarkerColIndex; @@ -47,6 +49,7 @@ class surface_impl : public AbstractRenderable { GLuint mSurfColorIndex; GLuint mSurfAlphaIndex; GLuint mSurfPVCIndex; + GLuint mSurfPVAIndex; std::map mVAOMap; @@ -65,6 +68,14 @@ class surface_impl : public AbstractRenderable { void render(const int pWindowId, const int pX, const int pY, const int pVPW, const int pVPH, const glm::mat4 &pTransform); + + inline void usePerVertexColors(const bool pFlag=true) { + mIsPVCOn = pFlag; + } + + inline void usePerVertexAlphas(const bool pFlag=true) { + mIsPVAOn = pFlag; + } }; class scatter3_impl : public surface_impl { From 5a73f5c732a5f7b2c69dcbadf91c3e18d3a3799d Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 1 Feb 2016 14:43:47 +0530 Subject: [PATCH 13/61] Fixed 2d & 3d plot classes hierarchy --- src/plot.cpp | 260 +++++++++++++++++++++++++++++++++++++++++++++++++ src/plot.hpp | 266 ++++++--------------------------------------------- 2 files changed, 288 insertions(+), 238 deletions(-) diff --git a/src/plot.cpp b/src/plot.cpp index 1b5d9c79..0ce4b2b7 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -14,6 +14,266 @@ using namespace std; +// identity matrix +static const glm::mat4 I(1.0f); + +namespace internal +{ + +void plot_impl::bindResources(const int pWindowId) +{ + if (mVAOMap.find(pWindowId) == mVAOMap.end()) { + GLuint vao = 0; + /* create a vertex array object + * with appropriate bindings */ + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + // attach vertices + glEnableVertexAttribArray(mPlotPointIndex); + glBindBuffer(GL_ARRAY_BUFFER, mVBO); + glVertexAttribPointer(mPlotPointIndex, mDimension, mGLType, GL_FALSE, 0, 0); + // attach colors + glEnableVertexAttribArray(mPlotColorIndex); + glBindBuffer(GL_ARRAY_BUFFER, mCBO); + glVertexAttribPointer(mPlotColorIndex, 3, GL_FLOAT, GL_FALSE, 0, 0); + // attach alphas + glEnableVertexAttribArray(mPlotAlphaIndex); + glBindBuffer(GL_ARRAY_BUFFER, mABO); + glVertexAttribPointer(mPlotAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); + glBindVertexArray(0); + /* store the vertex array object corresponding to + * the window instance in the map */ + mVAOMap[pWindowId] = vao; + } + + glBindVertexArray(mVAOMap[pWindowId]); +} + +void plot_impl::unbindResources() const +{ + glBindVertexArray(0); +} + +void plot_impl::computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput, + const int pX, const int pY, + const int pVPW, const int pVPH) +{ + float range_x = mRange[1] - mRange[0]; + float range_y = mRange[3] - mRange[2]; + float range_z = mRange[5] - mRange[4]; + // set scale to zero if input is constant array + // otherwise compute scale factor by standard equation + float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); + float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); + float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(range_z); + + float coor_offset_x = (-mRange[0] * graph_scale_x); + float coor_offset_y = (-mRange[2] * graph_scale_y); + float coor_offset_z = (-mRange[4] * graph_scale_z); + + glm::mat4 rMat = glm::rotate(I, -glm::radians(90.f), glm::vec3(1,0,0)); + glm::mat4 tMat = glm::translate(I, + glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)); + glm::mat4 sMat = glm::scale(I, + glm::vec3(1.0f * graph_scale_x, -1.0f * graph_scale_y, 1.0f * graph_scale_z)); + + glm::mat4 model= rMat * tMat * sMat; + + pOut = pInput * model; + glScissor(pX, pY, pVPW, pVPH); +} + +void plot_impl::bindDimSpecificUniforms() +{ + glUniform2fv(mPlotRangeIndex, 3, mRange); +} + +plot_impl::plot_impl(const uint pNumPoints, const fg::dtype pDataType, + const fg::PlotType pPlotType, const fg::MarkerType pMarkerType, const int pD) + : mDimension(pD), mNumPoints(pNumPoints), mDataType(pDataType), mGLType(dtype2gl(mDataType)), + mMarkerType(pMarkerType), mPlotType(pPlotType), mPlotProgram(-1), mMarkerProgram(-1), + mPlotMatIndex(-1), mPlotPVCOnIndex(-1), mPlotPVAOnIndex(-1), mPlotUColorIndex(-1), + mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), mPlotAlphaIndex(-1), + mMarkerPVCOnIndex(-1), mMarkerPVAOnIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), + mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) +{ + CheckGL("Begin plot_impl::plot_impl"); + mIsPVCOn = false; + mIsPVAOn = false; + + setColor(0, 1, 0, 1); + setLegend(std::string("")); + + if (mDimension==2) { + mPlotProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::histogram_fs.c_str()); + mMarkerProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::marker_fs.c_str()); + mPlotUColorIndex = glGetUniformLocation(mPlotProgram, "barColor"); + mVBOSize = 2*mNumPoints; + } else { + mPlotProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); + mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); + mPlotRangeIndex = glGetUniformLocation(mPlotProgram, "minmaxs"); + mVBOSize = 3*mNumPoints; + } + + mCBOSize = 3*mNumPoints; + mABOSize = mNumPoints; + + mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); + mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); + mPlotPVAOnIndex = glGetUniformLocation(mPlotProgram, "isPVAOn"); + mPlotPointIndex = glGetAttribLocation (mPlotProgram, "point"); + mPlotColorIndex = glGetAttribLocation (mPlotProgram, "color"); + mPlotAlphaIndex = glGetAttribLocation (mPlotProgram, "alpha"); + + mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); + mMarkerPVCOnIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); + mMarkerPVAOnIndex = glGetUniformLocation(mMarkerProgram, "isPVAOn"); + mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); + mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); + mMarkerPointIndex = glGetAttribLocation (mMarkerProgram, "point"); + mMarkerColorIndex = glGetAttribLocation (mMarkerProgram, "color"); + mMarkerAlphaIndex = glGetAttribLocation (mMarkerProgram, "alpha"); + +#define PLOT_CREATE_BUFFERS(type) \ + mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ + mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ + mABO = createBuffer(GL_ARRAY_BUFFER, mABOSize, NULL, GL_DYNAMIC_DRAW); \ + mVBOSize *= sizeof(type); \ + mCBOSize *= sizeof(float); \ + mABOSize *= sizeof(float); + + switch(mGLType) { + case GL_FLOAT : PLOT_CREATE_BUFFERS(float) ; break; + case GL_INT : PLOT_CREATE_BUFFERS(int) ; break; + case GL_UNSIGNED_INT : PLOT_CREATE_BUFFERS(uint) ; break; + case GL_SHORT : PLOT_CREATE_BUFFERS(short) ; break; + case GL_UNSIGNED_SHORT : PLOT_CREATE_BUFFERS(ushort); break; + case GL_UNSIGNED_BYTE : PLOT_CREATE_BUFFERS(float) ; break; + default: fg::TypeError("plot_impl::plot_impl", __LINE__, 1, mDataType); + } +#undef PLOT_CREATE_BUFFERS + CheckGL("End plot_impl::plot_impl"); +} + +plot_impl::~plot_impl() +{ + CheckGL("Begin plot_impl::~plot_impl"); + for (auto it = mVAOMap.begin(); it!=mVAOMap.end(); ++it) { + GLuint vao = it->second; + glDeleteVertexArrays(1, &vao); + } + glDeleteBuffers(1, &mVBO); + glDeleteBuffers(1, &mCBO); + glDeleteBuffers(1, &mABO); + glDeleteProgram(mPlotProgram); + glDeleteProgram(mMarkerProgram); + CheckGL("End plot_impl::~plot_impl"); +} + +void plot_impl::render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform) +{ + CheckGL("Begin plot_impl::render"); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_SCISSOR_TEST); + + glm::mat4 mvp(1.0); + this->computeTransformMat(mvp, pTransform, pX, pY, pVPW, pVPH); + + if (mPlotType == fg::FG_LINE) { + glUseProgram(mPlotProgram); + + this->bindDimSpecificUniforms(); + glUniformMatrix4fv(mPlotMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); + glUniform1i(mPlotPVCOnIndex, mIsPVCOn); + glUniform1i(mPlotPVAOnIndex, mIsPVAOn); + + plot_impl::bindResources(pWindowId); + glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); + plot_impl::unbindResources(); + + glUseProgram(0); + } + + if (mMarkerType != fg::FG_NONE) { + glEnable(GL_PROGRAM_POINT_SIZE); + glPointSize(10); + glUseProgram(mMarkerProgram); + + glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); + glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); + glUniform1i(mMarkerPVAOnIndex, mIsPVAOn); + glUniform1i(mMarkerTypeIndex, mMarkerType); + glUniform4fv(mMarkerColIndex, 1, mColor); + + plot_impl::bindResources(pWindowId); + glDrawArrays(GL_POINTS, 0, mNumPoints); + plot_impl::unbindResources(); + + glUseProgram(0); + glDisable(GL_PROGRAM_POINT_SIZE); + glPointSize(1); + } + + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + CheckGL("End plot_impl::render"); +} + +void plot2d_impl::computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput, + const int pX, const int pY, + const int pVPW, const int pVPH) +{ + float range_x = mRange[1] - mRange[0]; + float range_y = mRange[3] - mRange[2]; + // set scale to zero if input is constant array + // otherwise compute scale factor by standard equation + float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); + float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); + + float coor_offset_x = (-mRange[0] * graph_scale_x); + float coor_offset_y = (-mRange[2] * graph_scale_y); + + //FIXME: Using hard constants for now, find a way to get chart values + const float lMargin = 68; + const float rMargin = 8; + const float tMargin = 8; + const float bMargin = 32; + const float tickSize = 10; + + float viewWidth = pVPW - (lMargin + rMargin + tickSize/2); + float viewHeight = pVPH - (bMargin + tMargin + tickSize ); + float view_scale_x = viewWidth/pVPW; + float view_scale_y = viewHeight/pVPH; + + coor_offset_x *= view_scale_x; + coor_offset_y *= view_scale_y; + + float view_offset_x = (2.0f * (lMargin + tickSize/2 )/ pVPW ) ; + float view_offset_y = (2.0f * (bMargin + tickSize )/ pVPH ) ; + + glm::mat4 tMat = glm::translate(I, + glm::vec3(-1 + view_offset_x + coor_offset_x , -1 + view_offset_y + coor_offset_y, 0)); + pOut = glm::scale(tMat, + glm::vec3(graph_scale_x * view_scale_x , graph_scale_y * view_scale_y ,1)); + + pOut = pInput * pOut; + + glScissor(pX + lMargin + tickSize/2, pY+bMargin + tickSize/2, + pVPW - lMargin - rMargin - tickSize/2, + pVPH - bMargin - tMargin - tickSize/2); +} + +void plot2d_impl::bindDimSpecificUniforms() +{ + glUniform4fv(mPlotUColorIndex, 1, mColor); +} + +} + namespace fg { diff --git a/src/plot.hpp b/src/plot.hpp index 83596373..6040d721 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -27,9 +27,9 @@ namespace internal { -template class plot_impl : public AbstractRenderable { protected: + GLuint mDimension; /* plot points characteristics */ GLuint mNumPoints; fg::dtype mDataType; @@ -62,266 +62,56 @@ class plot_impl : public AbstractRenderable { /* bind and unbind helper functions * for rendering resources */ - void bindResources(const int pWindowId) - { - if (mVAOMap.find(pWindowId) == mVAOMap.end()) { - GLuint vao = 0; - /* create a vertex array object - * with appropriate bindings */ - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - // attach vertices - glEnableVertexAttribArray(mPlotPointIndex); - glBindBuffer(GL_ARRAY_BUFFER, mVBO); - if (PLOT_TYPE==fg::FG_2D) - glVertexAttribPointer(mPlotPointIndex, 2, mGLType, GL_FALSE, 0, 0); - else if (PLOT_TYPE==fg::FG_3D) - glVertexAttribPointer(mPlotPointIndex, 3, mGLType, GL_FALSE, 0, 0); - // attach colors - glEnableVertexAttribArray(mPlotColorIndex); - glBindBuffer(GL_ARRAY_BUFFER, mCBO); - glVertexAttribPointer(mPlotColorIndex, 3, GL_FLOAT, GL_FALSE, 0, 0); - // attach alphas - glEnableVertexAttribArray(mPlotAlphaIndex); - glBindBuffer(GL_ARRAY_BUFFER, mABO); - glVertexAttribPointer(mPlotAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); - glBindVertexArray(0); - /* store the vertex array object corresponding to - * the window instance in the map */ - mVAOMap[pWindowId] = vao; - } - - glBindVertexArray(mVAOMap[pWindowId]); - } - - void unbindResources() const - { - glBindVertexArray(0); - } - - void computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput, - const int pX, const int pY, - const int pVPW, const int pVPH) - { - // identity matrix - static const glm::mat4 I(1.0f); - - float range_x = mRange[1] - mRange[0]; - float range_y = mRange[3] - mRange[2]; - // set scale to zero if input is constant array - // otherwise compute scale factor by standard equation - float graph_scale_x = std::abs(range_x) < 1.0e-3 ? 0.0f : 2/(range_x); - float graph_scale_y = std::abs(range_y) < 1.0e-3 ? 0.0f : 2/(range_y); - - float coor_offset_x = (-mRange[0] * graph_scale_x); - float coor_offset_y = (-mRange[2] * graph_scale_y); - - if (PLOT_TYPE == fg::FG_3D) { - float range_z = mRange[5] - mRange[4]; - float graph_scale_z = std::abs(range_z) < 1.0e-3 ? 0.0f : 2/(range_z); - float coor_offset_z = (-mRange[4] * graph_scale_z); - - glm::mat4 rMat = glm::rotate(I, -glm::radians(90.f), glm::vec3(1,0,0)); - glm::mat4 tMat = glm::translate(I, - glm::vec3(-1 + coor_offset_x , -1 + coor_offset_y, -1 + coor_offset_z)); - glm::mat4 sMat = glm::scale(I, - glm::vec3(1.0f * graph_scale_x, -1.0f * graph_scale_y, 1.0f * graph_scale_z)); - - glm::mat4 model= rMat * tMat * sMat; + void bindResources(const int pWindowId); + void unbindResources() const; - pOut = pInput * model; - glScissor(pX, pY, pVPW, pVPH); - } else if (PLOT_TYPE == fg::FG_2D) { - //FIXME: Using hard constants for now, find a way to get chart values - const float lMargin = 68; - const float rMargin = 8; - const float tMargin = 8; - const float bMargin = 32; - const float tickSize = 10; - - float viewWidth = pVPW - (lMargin + rMargin + tickSize/2); - float viewHeight = pVPH - (bMargin + tMargin + tickSize ); - float view_scale_x = viewWidth/pVPW; - float view_scale_y = viewHeight/pVPH; - - coor_offset_x *= view_scale_x; - coor_offset_y *= view_scale_y; - - float view_offset_x = (2.0f * (lMargin + tickSize/2 )/ pVPW ) ; - float view_offset_y = (2.0f * (bMargin + tickSize )/ pVPH ) ; - - glm::mat4 tMat = glm::translate(I, - glm::vec3(-1 + view_offset_x + coor_offset_x , -1 + view_offset_y + coor_offset_y, 0)); - pOut = glm::scale(tMat, - glm::vec3(graph_scale_x * view_scale_x , graph_scale_y * view_scale_y ,1)); - - pOut = pInput * pOut; - - glScissor(pX + lMargin + tickSize/2, pY+bMargin + tickSize/2, - pVPW - lMargin - rMargin - tickSize/2, - pVPH - bMargin - tMargin - tickSize/2); - } - } + virtual void computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput, + const int pX, const int pY, + const int pVPW, const int pVPH); + virtual void bindDimSpecificUniforms(); // has to be called only after shaders are bound public: plot_impl(const uint pNumPoints, const fg::dtype pDataType, - const fg::PlotType pPlotType, const fg::MarkerType pMarkerType) - : mNumPoints(pNumPoints), mDataType(pDataType), mGLType(dtype2gl(mDataType)), - mMarkerType(pMarkerType), mPlotType(pPlotType), mPlotProgram(-1), mMarkerProgram(-1), - mPlotMatIndex(-1), mPlotPVCOnIndex(-1), mPlotPVAOnIndex(-1), mPlotUColorIndex(-1), - mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), mPlotAlphaIndex(-1), - mMarkerPVCOnIndex(-1), mMarkerPVAOnIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), - mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) - { - CheckGL("Begin plot_impl::plot_impl"); - mIsPVCOn = false; - mIsPVAOn = false; - - setColor(0, 1, 0, 1); - setLegend(std::string("")); - - if (PLOT_TYPE==fg::FG_2D) { - mPlotProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::histogram_fs.c_str()); - mMarkerProgram = initShaders(glsl::marker2d_vs.c_str(), glsl::marker_fs.c_str()); - mPlotUColorIndex = glGetUniformLocation(mPlotProgram, "barColor"); - mVBOSize = 2*mNumPoints; - } else if (PLOT_TYPE==fg::FG_3D) { - mPlotProgram = initShaders(glsl::plot3_vs.c_str(), glsl::plot3_fs.c_str()); - mMarkerProgram = initShaders(glsl::plot3_vs.c_str(), glsl::marker_fs.c_str()); - mPlotRangeIndex = glGetUniformLocation(mPlotProgram, "minmaxs"); - mVBOSize = 3*mNumPoints; - } - mCBOSize = 3*mNumPoints; - mABOSize = mNumPoints; - - mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); - mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); - mPlotPVAOnIndex = glGetUniformLocation(mPlotProgram, "isPVAOn"); - mPlotPointIndex = glGetAttribLocation (mPlotProgram, "point"); - mPlotColorIndex = glGetAttribLocation (mPlotProgram, "color"); - mPlotAlphaIndex = glGetAttribLocation (mPlotProgram, "alpha"); + const fg::PlotType pPlotType, const fg::MarkerType pMarkerType, + const int pDimension=3); + ~plot_impl(); - mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); - mMarkerPVCOnIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); - mMarkerPVAOnIndex = glGetUniformLocation(mMarkerProgram, "isPVAOn"); - mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); - mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); - mMarkerPointIndex = glGetAttribLocation (mMarkerProgram, "point"); - mMarkerColorIndex = glGetAttribLocation (mMarkerProgram, "color"); - mMarkerAlphaIndex = glGetAttribLocation (mMarkerProgram, "alpha"); - -#define PLOT_CREATE_BUFFERS(type) \ - mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ - mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ - mABO = createBuffer(GL_ARRAY_BUFFER, mABOSize, NULL, GL_DYNAMIC_DRAW); \ - mVBOSize *= sizeof(type); \ - mCBOSize *= sizeof(float); \ - mABOSize *= sizeof(float); - - switch(mGLType) { - case GL_FLOAT : PLOT_CREATE_BUFFERS(float) ; break; - case GL_INT : PLOT_CREATE_BUFFERS(int) ; break; - case GL_UNSIGNED_INT : PLOT_CREATE_BUFFERS(uint) ; break; - case GL_SHORT : PLOT_CREATE_BUFFERS(short) ; break; - case GL_UNSIGNED_SHORT : PLOT_CREATE_BUFFERS(ushort); break; - case GL_UNSIGNED_BYTE : PLOT_CREATE_BUFFERS(float) ; break; - default: fg::TypeError("plot_impl::plot_impl", __LINE__, 1, mDataType); - } -#undef PLOT_CREATE_BUFFERS - CheckGL("End plot_impl::plot_impl"); - } - - ~plot_impl() - { - CheckGL("Begin plot_impl::~plot_impl"); - for (auto it = mVAOMap.begin(); it!=mVAOMap.end(); ++it) { - GLuint vao = it->second; - glDeleteVertexArrays(1, &vao); - } - glDeleteBuffers(1, &mVBO); - glDeleteBuffers(1, &mCBO); - glDeleteBuffers(1, &mABO); - glDeleteProgram(mPlotProgram); - glDeleteProgram(mMarkerProgram); - CheckGL("End plot_impl::~plot_impl"); - } - - void render(const int pWindowId, - const int pX, const int pY, const int pVPW, const int pVPH, - const glm::mat4& pTransform) - { - CheckGL("Begin plot_impl::render"); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_SCISSOR_TEST); - - glm::mat4 mvp(1.0); - computeTransformMat(mvp, pTransform, pX, pY, pVPW, pVPH); - - if (mPlotType == fg::FG_LINE) { - glUseProgram(mPlotProgram); - - if (PLOT_TYPE==fg::FG_3D) { - glUniform2fv(mPlotRangeIndex, 3, mRange); - } - if (PLOT_TYPE==fg::FG_2D) { - glUniform4fv(mPlotUColorIndex, 1, mColor); - } - glUniformMatrix4fv(mPlotMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); - glUniform1i(mPlotPVCOnIndex, mIsPVCOn); - glUniform1i(mPlotPVAOnIndex, mIsPVAOn); - - plot_impl::bindResources(pWindowId); - glDrawArrays(GL_LINE_STRIP, 0, mNumPoints); - plot_impl::unbindResources(); - - glUseProgram(0); - } - - if (mMarkerType != fg::FG_NONE) { - glEnable(GL_PROGRAM_POINT_SIZE); - glPointSize(10); - glUseProgram(mMarkerProgram); - - glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); - glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); - glUniform1i(mMarkerPVAOnIndex, mIsPVAOn); - glUniform1i(mMarkerTypeIndex, mMarkerType); - glUniform4fv(mMarkerColIndex, 1, mColor); - - plot_impl::bindResources(pWindowId); - glDrawArrays(GL_POINTS, 0, mNumPoints); - plot_impl::unbindResources(); + virtual void render(const int pWindowId, + const int pX, const int pY, const int pVPW, const int pVPH, + const glm::mat4& pTransform); +}; - glUseProgram(0); - glDisable(GL_PROGRAM_POINT_SIZE); - glPointSize(1); - } +class plot2d_impl : public plot_impl { + protected: + void computeTransformMat(glm::mat4& pOut, const glm::mat4 pInput, + const int pX, const int pY, + const int pVPW, const int pVPH) override; + void bindDimSpecificUniforms() override; // has to be called only after shaders are bound - glDisable(GL_SCISSOR_TEST); - glDisable(GL_BLEND); - CheckGL("End plot_impl::render"); - } + public: + plot2d_impl(const uint pNumPoints, const fg::dtype pDataType, + const fg::PlotType pPlotType, const fg::MarkerType pMarkerType) + : plot_impl(pNumPoints, pDataType, pPlotType, pMarkerType, 2) {} }; class _Plot { private: - std::shared_ptr mPlot; + std::shared_ptr mPlot; public: _Plot(const uint pNumPoints, const fg::dtype pDataType, const fg::PlotType pPlotType, const fg::MarkerType pMarkerType, const fg::ChartType pChartType) { if (pChartType == fg::FG_2D) { - mPlot = std::make_shared< plot_impl >(pNumPoints, pDataType, + mPlot = std::make_shared< plot2d_impl >(pNumPoints, pDataType, pPlotType, pMarkerType); } else { - mPlot = std::make_shared< plot_impl >(pNumPoints, pDataType, + mPlot = std::make_shared< plot_impl >(pNumPoints, pDataType, pPlotType, pMarkerType); } } - inline const std::shared_ptr& impl() const { + inline const std::shared_ptr& impl() const { return mPlot; } From d569c0e7d02d6a0dd1f3f5b4c404cab6fcd3f0f2 Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 1 Feb 2016 15:30:15 +0530 Subject: [PATCH 14/61] Moved axes labels to outside chart area Also, moved Y axis label to the right side to have consistent position for the label irrespective of the tick labels displayed. --- src/chart.cpp | 19 +++++++++++++------ src/plot.hpp | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/chart.cpp b/src/chart.cpp index abd383ff..a06575b9 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -325,10 +325,18 @@ void chart2d_impl::generateTickLabels() int ticksLeft = mTickCount/2; /* push tick points for y axis */ mYText.push_back(toString(ymid)); + size_t maxYLabelWidth = 0; for (int i = 1; i <= ticksLeft; i++) { - mYText.push_back(toString(ymid + i*-ystep)); - mYText.push_back(toString(ymid + i*ystep)); + std::string temp = toString(ymid + i*-ystep); + mYText.push_back(temp); + maxYLabelWidth = std::max(maxYLabelWidth, temp.length()); + temp = toString(ymid + i*ystep); + mYText.push_back(temp); + maxYLabelWidth = std::max(maxYLabelWidth, temp.length()); } + + mLeftMargin = std::max((int)maxYLabelWidth, mLeftMargin); + /* push tick points for x axis */ mXText.push_back(toString(xmid)); for (int i = 1; i <= ticksLeft; i++) { @@ -338,7 +346,7 @@ void chart2d_impl::generateTickLabels() } chart2d_impl::chart2d_impl() - :AbstractChart(68, 8, 8, 32) { + : AbstractChart(64, 32, 8, 44) { generateChartData(); } @@ -418,16 +426,15 @@ void chart2d_impl::render(const int pWindowId, /* render chart axes titles */ if (!mYTitle.empty()) { glm::vec4 res = trans * glm::vec4(-1.0f, 0.0f, 0.0f, 1.0f); - pos[0] = w*(res.x+1.0f)/2.0f; + pos[0] = w - CHART2D_FONT_SIZE; pos[1] = h*(res.y+1.0f)/2.0f; - pos[0] += (mTickSize * (w/pVPW)); fonter->render(pWindowId, pos, WHITE, mYTitle.c_str(), CHART2D_FONT_SIZE, true); } if (!mXTitle.empty()) { glm::vec4 res = trans * glm::vec4(0.0f, -1.0f, 0.0f, 1.0f); pos[0] = w*(res.x+1.0f)/2.0f; pos[1] = h*(res.y+1.0f)/2.0f; - pos[1] += (mTickSize * (h/pVPH)); + pos[1] -= (4*mTickSize * (h/pVPH)); fonter->render(pWindowId, pos, WHITE, mXTitle.c_str(), CHART2D_FONT_SIZE); } diff --git a/src/plot.hpp b/src/plot.hpp index 6040d721..272ef7d1 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -39,7 +39,7 @@ class plot_impl : public AbstractRenderable { /* OpenGL Objects */ GLuint mPlotProgram; GLuint mMarkerProgram; - /* shaderd variable index locations */ + /* shader variable index locations */ GLuint mPlotMatIndex; GLuint mPlotPVCOnIndex; GLuint mPlotPVAOnIndex; From c9e6f2155a4aab330eb916f4fa882cfdd6dac916 Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 1 Feb 2016 15:43:19 +0530 Subject: [PATCH 15/61] Corrected axes tick alignments w.r.t to ticks X axis tick labels are centered around the decimal point now. --- src/chart.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/chart.cpp b/src/chart.cpp index a06575b9..a3260fd9 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -97,12 +97,16 @@ void AbstractChart::renderTickLabels( /* offset based on text size to align * text center with tick mark position */ if(pCoordsOffset < mTickCount) { - pos[0] -= ((CHART2D_FONT_SIZE*it->length()/2.0f)); + // offset for y axis labels + pos[0] -= (CHART2D_FONT_SIZE*it->length()/2.0f+mTickSize/2); }else if(pCoordsOffset >= mTickCount && pCoordsOffset < 2*mTickCount) { - pos[1] -= ((CHART2D_FONT_SIZE)); + // offset for x axis labels + pos[0] -= (CHART2D_FONT_SIZE*it->length()/4.0f); + pos[1] -= (CHART2D_FONT_SIZE*1.32); }else { - pos[0] -= ((CHART2D_FONT_SIZE*it->length()/2.0f)); - pos[1] -= ((CHART2D_FONT_SIZE)); + // offsets for 3d chart axes ticks + pos[0] -= (CHART2D_FONT_SIZE*it->length()/2.0f); + pos[1] -= (CHART2D_FONT_SIZE); } fonter->render(pWindowId, pos, WHITE, it->c_str(), CHART2D_FONT_SIZE); } From 9acad904be193c96f55ec62759c5fea6b31bd22a Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 1 Feb 2016 22:26:37 +0530 Subject: [PATCH 16/61] support for per vertex marker size in scatter plots Also modified COPY helper headers to be more generic where the copy functions directly take OpenGL buffer object identifiers and the corresponding buffer size in bytes instead of Forge objects. --- examples/cpu/histogram.cpp | 4 +-- examples/cpu/plot3.cpp | 4 +-- examples/cpu/plotting.cpp | 8 ++--- examples/cpu/surface.cpp | 4 +-- examples/cuda/histogram.cu | 8 ++--- examples/cuda/plot3.cu | 4 +-- examples/cuda/plotting.cu | 8 ++--- examples/cuda/surface.cu | 4 +-- examples/opencl/histogram.cpp | 8 ++--- examples/opencl/plot3.cpp | 4 +-- examples/opencl/plotting.cpp | 8 ++--- examples/opencl/surface.cpp | 4 +-- include/CPUCopy.hpp | 39 ++++----------------- include/CUDACopy.hpp | 36 ++++--------------- include/OpenCLCopy.hpp | 45 +++++------------------- include/fg/plot.h | 28 +++++++++++++-- src/plot.cpp | 66 +++++++++++++++++++++++++++++------ src/plot.hpp | 24 +++++++++++++ src/shaders/marker2d_vs.glsl | 4 +++ src/shaders/plot3_vs.glsl | 4 +++ 20 files changed, 168 insertions(+), 146 deletions(-) diff --git a/examples/cpu/histogram.cpp b/examples/cpu/histogram.cpp index cc2e233c..a75ccddd 100644 --- a/examples/cpu/histogram.cpp +++ b/examples/cpu/histogram.cpp @@ -104,8 +104,8 @@ int main(void) std::vector colArray(3*NBINS, 0.0f); populateBins(bmp, histArray.data(), NBINS, colArray.data()); - fg::copy(hist, histArray.data()); - fg::copy(hist, colArray.data(), fg::FG_COLOR_BUFFER); + fg::copy(hist.vertices(), hist.verticesSize(), histArray.data()); + fg::copy(hist.colors(), hist.colorsSize(), colArray.data()); wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); wnd.draw(1, 0, chart, "Histogram of Noisy Image"); diff --git a/examples/cpu/plot3.cpp b/examples/cpu/plot3.cpp index 05d1a7f8..5e8b6340 100644 --- a/examples/cpu/plot3.cpp +++ b/examples/cpu/plot3.cpp @@ -71,12 +71,12 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - copy(plot3, &function[0]); + fg::copy(plot3.vertices(), plot3.verticesSize(), &function[0]); do { t+=0.01; gen_curve(t, DX, function); - copy(plot3, &function[0]); + fg::copy(plot3.vertices(), plot3.verticesSize(), &function[0]); wnd.draw(chart); } while(!wnd.close()); diff --git a/examples/cpu/plotting.cpp b/examples/cpu/plotting.cpp index 05ef3ef2..2ab3655f 100644 --- a/examples/cpu/plotting.cpp +++ b/examples/cpu/plotting.cpp @@ -94,10 +94,10 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - copy(plt0, &sinData[0]); - copy(plt1, &cosData[0]); - copy(plt2, &tanData[0]); - copy(plt3, &logData[0]); + fg::copy(plt0.vertices(), plt0.verticesSize(), &sinData[0]); + fg::copy(plt1.vertices(), plt1.verticesSize(), &cosData[0]); + fg::copy(plt2.vertices(), plt2.verticesSize(), &tanData[0]); + fg::copy(plt3.vertices(), plt3.verticesSize(), &logData[0]); do { wnd.draw(chart); diff --git a/examples/cpu/surface.cpp b/examples/cpu/surface.cpp index 88921676..c48ccddc 100644 --- a/examples/cpu/surface.cpp +++ b/examples/cpu/surface.cpp @@ -76,12 +76,12 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - copy(surf, &function[0]); + fg::copy(surf.vertices(), surf.verticesSize(), &function[0]); do { t+=0.07; gen_surface(t, DX, function); - copy(surf, &function[0]); + fg::copy(surf.vertices(), surf.verticesSize(), &function[0]); wnd.draw(chart); } while(!wnd.close()); diff --git a/examples/cuda/histogram.cu b/examples/cuda/histogram.cu index 5d6a66df..797b3c4e 100644 --- a/examples/cuda/histogram.cu +++ b/examples/cuda/histogram.cu @@ -114,8 +114,8 @@ int main(void) generateNoisyImage(dev_out); generateHistogram(dev_out, hist_out, hist_colors); fg::copy(img, dev_out); - fg::copy(hist, hist_out); - fg::copy(hist, hist_colors, fg::FG_COLOR_BUFFER); + fg::copy(hist.vertices(), hist_out); + fg::copy(hist.colors(), hist_colors); do { wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); @@ -127,8 +127,8 @@ int main(void) fg::copy(img, dev_out); // limit histogram update frequency if(fmod(persistance, 0.5f) < 0.01) { - fg::copy(hist, hist_out); - fg::copy(hist, hist_colors, fg::FG_COLOR_BUFFER); + fg::copy(hist.vertices(), hist_out); + fg::copy(hist.colors(), hist_colors); } } while(!wnd.close()); diff --git a/examples/cuda/plot3.cu b/examples/cuda/plot3.cu index 228f2644..eaad66c4 100644 --- a/examples/cuda/plot3.cu +++ b/examples/cuda/plot3.cu @@ -62,13 +62,13 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(plot3, dev_out); + fg::copy(plot3.vertices(), dev_out); do { t+=0.01; kernel(t, DX, dev_out); - fg::copy(plot3, dev_out); + fg::copy(plot3.vertices(), dev_out); wnd.draw(chart); } while(!wnd.close()); diff --git a/examples/cuda/plotting.cu b/examples/cuda/plotting.cu index 732efeeb..a9824da2 100644 --- a/examples/cuda/plotting.cu +++ b/examples/cuda/plotting.cu @@ -82,10 +82,10 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(plt0, sin_out); - fg::copy(plt1, cos_out); - fg::copy(plt2, tan_out); - fg::copy(plt3, log_out); + fg::copy(plt0.vertices(), sin_out); + fg::copy(plt1.vertices(), cos_out); + fg::copy(plt2.vertices(), tan_out); + fg::copy(plt3.vertices(), log_out); do { wnd.draw(chart); diff --git a/examples/cuda/surface.cu b/examples/cuda/surface.cu index fc21bfe9..05df0c12 100644 --- a/examples/cuda/surface.cu +++ b/examples/cuda/surface.cu @@ -57,12 +57,12 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(surf, dev_out); + fg::copy(surf.vertices(), dev_out); do { t+=0.07; kernel(t, DX, dev_out); - fg::copy(surf, dev_out); + fg::copy(surf.vertices(), dev_out); wnd.draw(chart); } while(!wnd.close()); diff --git a/examples/opencl/histogram.cpp b/examples/opencl/histogram.cpp index b1af4947..424d532c 100644 --- a/examples/opencl/histogram.cpp +++ b/examples/opencl/histogram.cpp @@ -298,8 +298,8 @@ int main(void) * along with the library to help with this task */ fg::copy(img, devOut, queue); - fg::copy(hist, histOut, queue); - fg::copy(hist, colors, queue, fg::FG_COLOR_BUFFER); + fg::copy(hist.vertices(), hist.verticesSize(), histOut, queue); + fg::copy(hist.vertices(), hist.verticesSize(), colors, queue); do { wnd.draw(0, 0, img, "Dynamic Perlin Noise" ); @@ -312,8 +312,8 @@ int main(void) // limit histogram update frequency if (fmod(persistance, 0.4f) < 0.02f) { - fg::copy(hist, histOut, queue); - fg::copy(hist, colors, queue, fg::FG_COLOR_BUFFER); + fg::copy(hist.vertices(), hist.verticesSize(), histOut, queue); + fg::copy(hist.vertices(), hist.verticesSize(), colors, queue); } } while(!wnd.close()); diff --git a/examples/opencl/plot3.cpp b/examples/opencl/plot3.cpp index a25286fa..d312e002 100644 --- a/examples/opencl/plot3.cpp +++ b/examples/opencl/plot3.cpp @@ -152,12 +152,12 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(plot3, devOut, queue); + fg::copy(plot3.vertices(), plot3.verticesSize(), devOut, queue); do { t+=0.01; kernel(devOut, queue, t); - fg::copy(plot3, devOut, queue); + fg::copy(plot3.vertices(), plot3.verticesSize(), devOut, queue); wnd.draw(chart); } while(!wnd.close()); }catch (fg::Error err) { diff --git a/examples/opencl/plotting.cpp b/examples/opencl/plotting.cpp index 3de6c20b..c32e86df 100644 --- a/examples/opencl/plotting.cpp +++ b/examples/opencl/plotting.cpp @@ -181,10 +181,10 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(plt0, sinOut, queue); - fg::copy(plt1, cosOut, queue); - fg::copy(plt2, tanOut, queue); - fg::copy(plt3, logOut, queue); + fg::copy(plt0.vertices(), plt0.verticesSize(), sinOut, queue); + fg::copy(plt1.vertices(), plt1.verticesSize(), cosOut, queue); + fg::copy(plt2.vertices(), plt2.verticesSize(), tanOut, queue); + fg::copy(plt3.vertices(), plt3.verticesSize(), logOut, queue); do { wnd.draw(chart); diff --git a/examples/opencl/surface.cpp b/examples/opencl/surface.cpp index d9b16ccf..3b508499 100644 --- a/examples/opencl/surface.cpp +++ b/examples/opencl/surface.cpp @@ -159,12 +159,12 @@ int main(void) * memory to display memory, Forge provides copy headers * along with the library to help with this task */ - fg::copy(surf, devOut, queue); + fg::copy(surf.vertices(), surf.verticesSize(), devOut, queue); do { t+=0.07; kernel(devOut, queue, t); - fg::copy(surf, devOut, queue); + fg::copy(surf.vertices(), surf.verticesSize(), devOut, queue); wnd.draw(chart); } while(!wnd.close()); }catch (fg::Error err) { diff --git a/include/CPUCopy.hpp b/include/CPUCopy.hpp index a3ae256c..0864fe54 100644 --- a/include/CPUCopy.hpp +++ b/include/CPUCopy.hpp @@ -13,12 +13,6 @@ namespace fg { -enum BufferType { - FG_VERTEX_BUFFER = 0, - FG_COLOR_BUFFER = 1, - FG_ALPHA_BUFFER = 2 -}; - template void copy(fg::Image& out, const T * dataPtr) { @@ -28,35 +22,14 @@ void copy(fg::Image& out, const T * dataPtr) } /* - * Below functions takes any renderable forge object that has following member functions - * defined - * - * `unsigned Renderable::vertices() const;` - * `unsigned Renderable::verticesSize() const;` - * - * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill + * Below functions expects OpenGL resource Id and size in bytes to copy the data from + * cpu memory location to graphics memory */ -template -void copy(Renderable& out, const T * dataPtr, const BufferType bufferType=FG_VERTEX_BUFFER) +template +void copy(const int resourceId, const size_t resourceSize, const T * dataPtr) { - unsigned rId = 0; - size_t size = 0; - switch(bufferType) { - case FG_VERTEX_BUFFER: - rId = out.vertices(); - size = out.verticesSize(); - break; - case FG_COLOR_BUFFER: - rId = out.colors(); - size = out.colorsSize(); - break; - case FG_ALPHA_BUFFER: - rId = out.alphas(); - size = out.alphasSize(); - break; - } - glBindBuffer(GL_ARRAY_BUFFER, rId); - glBufferSubData(GL_ARRAY_BUFFER, 0, size, dataPtr); + glBindBuffer(GL_ARRAY_BUFFER, resourceId); + glBufferSubData(GL_ARRAY_BUFFER, 0, resourceSize, dataPtr); glBindBuffer(GL_ARRAY_BUFFER, 0); } diff --git a/include/CUDACopy.hpp b/include/CUDACopy.hpp index df318af9..b8aa1380 100644 --- a/include/CUDACopy.hpp +++ b/include/CUDACopy.hpp @@ -26,12 +26,6 @@ static void handleCUDAError(cudaError_t err, const char *file, int line) namespace fg { -enum BufferType { - FG_VERTEX_BUFFER = 0, - FG_COLOR_BUFFER = 1, - FG_ALPHA_BUFFER = 2 -}; - template void copy(fg::Image& out, const T * devicePtr) { @@ -49,33 +43,15 @@ void copy(fg::Image& out, const T * devicePtr) } /* - * Below functions takes any renderable forge object that has following member functions - * defined - * - * `unsigned Renderable::vertices() const;` - * `unsigned Renderable::verticesSize() const;` - * - * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill + * Below functions expects OpenGL resource Id and size in bytes to copy the data from + * CUDA device memory location to graphics memory */ -template -void copy(Renderable& out, const T * devicePtr, const BufferType bufferType=FG_VERTEX_BUFFER) +template +void copy(const int resourceId, const T * devicePtr) { - unsigned rId = 0; - switch(bufferType) { - case FG_VERTEX_BUFFER: - rId = out.vertices(); - break; - case FG_COLOR_BUFFER: - rId = out.colors(); - break; - case FG_ALPHA_BUFFER: - rId = out.alphas(); - break; - } - cudaGraphicsResource *cudaVBOResource; - CUDA_ERROR_CHECK(cudaGraphicsGLRegisterBuffer(&cudaVBOResource, rId, cudaGraphicsMapFlagsWriteDiscard)); - + CUDA_ERROR_CHECK(cudaGraphicsGLRegisterBuffer(&cudaVBOResource, resourceId, + cudaGraphicsMapFlagsWriteDiscard)); size_t num_bytes; T* vboDevicePtr = NULL; diff --git a/include/OpenCLCopy.hpp b/include/OpenCLCopy.hpp index 19f2efc4..de19123e 100644 --- a/include/OpenCLCopy.hpp +++ b/include/OpenCLCopy.hpp @@ -13,13 +13,8 @@ namespace fg { -enum BufferType { - FG_VERTEX_BUFFER = 0, - FG_COLOR_BUFFER = 1, - FG_ALPHA_BUFFER = 2 -}; - -static void copy(fg::Image& out, const cl::Buffer& in, const cl::CommandQueue& queue) +static +void copy(fg::Image& out, const cl::Buffer& in, const cl::CommandQueue& queue) { cl::BufferGL pboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, out.pbo(), NULL); @@ -34,43 +29,21 @@ static void copy(fg::Image& out, const cl::Buffer& in, const cl::CommandQueue& q } /* - * Below functions takes any renderable forge object that has following member functions - * defined - * - * `unsigned Renderable::vbo() const;` - * `unsigned Renderable::size() const;` - * - * Currently fg::Plot, fg::Histogram objects in Forge library fit the bill + * Below functions expects OpenGL resource Id and size in bytes to copy the data from + * OpenCL Buffer to graphics memory */ -template -void copy(Renderable& out, const cl::Buffer& in, const cl::CommandQueue& queue, - const BufferType bufferType=FG_VERTEX_BUFFER) +static +void copy(const int resourceId, const size_t resourceSize, + const cl::Buffer& in, const cl::CommandQueue& queue) { - unsigned rId = 0; - size_t size = 0; - switch(bufferType) { - case FG_VERTEX_BUFFER: - rId = out.vertices(); - size = out.verticesSize(); - break; - case FG_COLOR_BUFFER: - rId = out.colors(); - size = out.colorsSize(); - break; - case FG_ALPHA_BUFFER: - rId = out.alphas(); - size = out.alphasSize(); - break; - } - - cl::BufferGL vboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, rId, NULL); + cl::BufferGL vboMapBuffer(queue.getInfo(), CL_MEM_WRITE_ONLY, resourceId, NULL); std::vector shared_objects; shared_objects.push_back(vboMapBuffer); glFinish(); queue.enqueueAcquireGLObjects(&shared_objects); - queue.enqueueCopyBuffer(in, vboMapBuffer, 0, 0, size, NULL, NULL); + queue.enqueueCopyBuffer(in, vboMapBuffer, 0, 0, resourceSize, NULL, NULL); queue.finish(); queue.enqueueReleaseGLObjects(&shared_objects); } diff --git a/include/fg/plot.h b/include/fg/plot.h index 5ffb3e62..b1b9bb33 100644 --- a/include/fg/plot.h +++ b/include/fg/plot.h @@ -85,6 +85,16 @@ class Plot { */ FGAPI void setLegend(const std::string& pLegend); + /** + Set global marker size + + This size will be used for rendering markers if no per vertex marker sizes are provided. + This value defaults to 10 + + \param[in] pMarkerSize is the target marker size for scatter plots or line plots with markers + */ + FGAPI void setMarkerSize(const float pMarkerSize); + /** Get the OpenGL buffer object identifier for vertices @@ -106,6 +116,13 @@ class Plot { */ FGAPI uint alphas() const; + /** + Get the OpenGL buffer object identifier for markers sizes, per vertex + + \return OpenGL VBO resource id. + */ + FGAPI uint markers() const; + /** Get the OpenGL Vertex Buffer Object resource size @@ -114,19 +131,26 @@ class Plot { FGAPI uint verticesSize() const; /** - Get the OpenGL Vertex Buffer Object resource size + Get the OpenGL colors Buffer Object resource size \return colors buffer object size in bytes */ FGAPI uint colorsSize() const; /** - Get the OpenGL Vertex Buffer Object resource size + Get the OpenGL alpha Buffer Object resource size \return alpha buffer object size in bytes */ FGAPI uint alphasSize() const; + /** + Get the OpenGL markers Buffer Object resource size + + \return alpha buffer object size in bytes + */ + FGAPI uint markersSize() const; + /** Get the handle to internal implementation of plot */ diff --git a/src/plot.cpp b/src/plot.cpp index 0ce4b2b7..e9c466b0 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -40,6 +40,10 @@ void plot_impl::bindResources(const int pWindowId) glEnableVertexAttribArray(mPlotAlphaIndex); glBindBuffer(GL_ARRAY_BUFFER, mABO); glVertexAttribPointer(mPlotAlphaIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); + // attach radii + glEnableVertexAttribArray(mMarkerRadiiIndex); + glBindBuffer(GL_ARRAY_BUFFER, mRBO); + glVertexAttribPointer(mMarkerRadiiIndex, 1, GL_FLOAT, GL_FALSE, 0, 0); glBindVertexArray(0); /* store the vertex array object corresponding to * the window instance in the map */ @@ -90,12 +94,13 @@ void plot_impl::bindDimSpecificUniforms() plot_impl::plot_impl(const uint pNumPoints, const fg::dtype pDataType, const fg::PlotType pPlotType, const fg::MarkerType pMarkerType, const int pD) - : mDimension(pD), mNumPoints(pNumPoints), mDataType(pDataType), mGLType(dtype2gl(mDataType)), - mMarkerType(pMarkerType), mPlotType(pPlotType), mPlotProgram(-1), mMarkerProgram(-1), - mPlotMatIndex(-1), mPlotPVCOnIndex(-1), mPlotPVAOnIndex(-1), mPlotUColorIndex(-1), - mPlotRangeIndex(-1), mPlotPointIndex(-1), mPlotColorIndex(-1), mPlotAlphaIndex(-1), - mMarkerPVCOnIndex(-1), mMarkerPVAOnIndex(-1), mMarkerTypeIndex(-1), mMarkerColIndex(-1), - mMarkerMatIndex(-1), mMarkerPointIndex(-1), mMarkerColorIndex(-1), mMarkerAlphaIndex(-1) + : mDimension(pD), mMarkerSize(10), mNumPoints(pNumPoints), mDataType(pDataType), + mGLType(dtype2gl(mDataType)), mMarkerType(pMarkerType), mPlotType(pPlotType), mIsPVROn(false), + mPlotProgram(-1), mMarkerProgram(-1), mRBO(-1), mPlotMatIndex(-1), mPlotPVCOnIndex(-1), + mPlotPVAOnIndex(-1), mPlotUColorIndex(-1), mPlotRangeIndex(-1), mPlotPointIndex(-1), + mPlotColorIndex(-1), mPlotAlphaIndex(-1), mMarkerPVCOnIndex(-1), mMarkerPVAOnIndex(-1), + mMarkerTypeIndex(-1), mMarkerColIndex(-1), mMarkerMatIndex(-1), mMarkerPointIndex(-1), + mMarkerColorIndex(-1), mMarkerAlphaIndex(-1), mMarkerRadiiIndex(-1) { CheckGL("Begin plot_impl::plot_impl"); mIsPVCOn = false; @@ -118,6 +123,7 @@ plot_impl::plot_impl(const uint pNumPoints, const fg::dtype pDataType, mCBOSize = 3*mNumPoints; mABOSize = mNumPoints; + mRBOSize = mNumPoints; mPlotMatIndex = glGetUniformLocation(mPlotProgram, "transform"); mPlotPVCOnIndex = glGetUniformLocation(mPlotProgram, "isPVCOn"); @@ -129,19 +135,24 @@ plot_impl::plot_impl(const uint pNumPoints, const fg::dtype pDataType, mMarkerMatIndex = glGetUniformLocation(mMarkerProgram, "transform"); mMarkerPVCOnIndex = glGetUniformLocation(mMarkerProgram, "isPVCOn"); mMarkerPVAOnIndex = glGetUniformLocation(mMarkerProgram, "isPVAOn"); + mMarkerPVROnIndex = glGetUniformLocation(mMarkerProgram, "isPVROn"); mMarkerTypeIndex = glGetUniformLocation(mMarkerProgram, "marker_type"); mMarkerColIndex = glGetUniformLocation(mMarkerProgram, "marker_color"); + mMarkerPSizeIndex = glGetUniformLocation(mMarkerProgram, "psize"); mMarkerPointIndex = glGetAttribLocation (mMarkerProgram, "point"); mMarkerColorIndex = glGetAttribLocation (mMarkerProgram, "color"); mMarkerAlphaIndex = glGetAttribLocation (mMarkerProgram, "alpha"); + mMarkerRadiiIndex = glGetAttribLocation (mMarkerProgram, "pointsize"); #define PLOT_CREATE_BUFFERS(type) \ mVBO = createBuffer(GL_ARRAY_BUFFER, mVBOSize, NULL, GL_DYNAMIC_DRAW); \ mCBO = createBuffer(GL_ARRAY_BUFFER, mCBOSize, NULL, GL_DYNAMIC_DRAW); \ mABO = createBuffer(GL_ARRAY_BUFFER, mABOSize, NULL, GL_DYNAMIC_DRAW); \ + mRBO = createBuffer(GL_ARRAY_BUFFER, mRBOSize, NULL, GL_DYNAMIC_DRAW); \ mVBOSize *= sizeof(type); \ mCBOSize *= sizeof(float); \ - mABOSize *= sizeof(float); + mABOSize *= sizeof(float); \ + mRBOSize *= sizeof(float); switch(mGLType) { case GL_FLOAT : PLOT_CREATE_BUFFERS(float) ; break; @@ -171,14 +182,31 @@ plot_impl::~plot_impl() CheckGL("End plot_impl::~plot_impl"); } +void plot_impl::setMarkerSize(const float pMarkerSize) +{ + mMarkerSize = pMarkerSize; +} + +GLuint plot_impl::markers() +{ + mIsPVROn = true; + return mRBO; +} + +size_t plot_impl::markersSizes() const +{ + return mRBOSize; +} + void plot_impl::render(const int pWindowId, const int pX, const int pY, const int pVPW, const int pVPH, const glm::mat4& pTransform) { CheckGL("Begin plot_impl::render"); + glEnable(GL_SCISSOR_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_SCISSOR_TEST); + glDepthMask(GL_FALSE); glm::mat4 mvp(1.0); this->computeTransformMat(mvp, pTransform, pX, pY, pVPW, pVPH); @@ -200,14 +228,15 @@ void plot_impl::render(const int pWindowId, if (mMarkerType != fg::FG_NONE) { glEnable(GL_PROGRAM_POINT_SIZE); - glPointSize(10); glUseProgram(mMarkerProgram); glUniformMatrix4fv(mMarkerMatIndex, 1, GL_FALSE, glm::value_ptr(mvp)); glUniform1i(mMarkerPVCOnIndex, mIsPVCOn); glUniform1i(mMarkerPVAOnIndex, mIsPVAOn); + glUniform1i(mMarkerPVROnIndex, mIsPVROn); glUniform1i(mMarkerTypeIndex, mMarkerType); glUniform4fv(mMarkerColIndex, 1, mColor); + glUniform1f(mMarkerPSizeIndex, mMarkerSize); plot_impl::bindResources(pWindowId); glDrawArrays(GL_POINTS, 0, mNumPoints); @@ -215,11 +244,11 @@ void plot_impl::render(const int pWindowId, glUseProgram(0); glDisable(GL_PROGRAM_POINT_SIZE); - glPointSize(1); } - glDisable(GL_SCISSOR_TEST); glDisable(GL_BLEND); + glDepthMask(GL_TRUE); + glDisable(GL_SCISSOR_TEST); CheckGL("End plot_impl::render"); } @@ -313,6 +342,11 @@ void Plot::setLegend(const std::string& pLegend) mValue->setLegend(pLegend); } +void Plot::setMarkerSize(const float pMarkerSize) +{ + mValue->setMarkerSize(pMarkerSize); +} + uint Plot::vertices() const { return mValue->vbo(); @@ -328,6 +362,11 @@ uint Plot::alphas() const return mValue->abo(); } +uint Plot::markers() const +{ + return mValue->mbo(); +} + uint Plot::verticesSize() const { return (uint)mValue->vboSize(); @@ -343,6 +382,11 @@ uint Plot::alphasSize() const return (uint)mValue->aboSize(); } +uint Plot::markersSize() const +{ + return (uint)mValue->mboSize(); +} + internal::_Plot* Plot::get() const { return mValue; diff --git a/src/plot.hpp b/src/plot.hpp index 272ef7d1..504bb063 100644 --- a/src/plot.hpp +++ b/src/plot.hpp @@ -30,15 +30,19 @@ namespace internal class plot_impl : public AbstractRenderable { protected: GLuint mDimension; + GLfloat mMarkerSize; /* plot points characteristics */ GLuint mNumPoints; fg::dtype mDataType; GLenum mGLType; fg::MarkerType mMarkerType; fg::PlotType mPlotType; + bool mIsPVROn; /* OpenGL Objects */ GLuint mPlotProgram; GLuint mMarkerProgram; + GLuint mRBO; + size_t mRBOSize; /* shader variable index locations */ GLuint mPlotMatIndex; GLuint mPlotPVCOnIndex; @@ -51,12 +55,15 @@ class plot_impl : public AbstractRenderable { GLuint mMarkerPVCOnIndex; GLuint mMarkerPVAOnIndex; + GLuint mMarkerPVROnIndex; GLuint mMarkerTypeIndex; GLuint mMarkerColIndex; GLuint mMarkerMatIndex; + GLuint mMarkerPSizeIndex; GLuint mMarkerPointIndex; GLuint mMarkerColorIndex; GLuint mMarkerAlphaIndex; + GLuint mMarkerRadiiIndex; std::map mVAOMap; @@ -76,6 +83,11 @@ class plot_impl : public AbstractRenderable { const int pDimension=3); ~plot_impl(); + void setMarkerSize(const float pMarkerSize); + + GLuint markers(); + size_t markersSizes() const; + virtual void render(const int pWindowId, const int pX, const int pY, const int pVPW, const int pVPH, const glm::mat4& pTransform); @@ -124,6 +136,10 @@ class _Plot { mPlot->setLegend(pLegend); } + inline void setMarkerSize(const float pMarkerSize) { + mPlot->setMarkerSize(pMarkerSize); + } + inline GLuint vbo() const { return mPlot->vbo(); } @@ -136,6 +152,10 @@ class _Plot { return mPlot->abo(); } + inline GLuint mbo() const { + return mPlot->markers(); + } + inline size_t vboSize() const { return mPlot->vboSize(); } @@ -148,6 +168,10 @@ class _Plot { return mPlot->aboSize(); } + inline size_t mboSize() const { + return mPlot->markersSizes(); + } + inline void render(const int pWindowId, const int pX, const int pY, const int pVPW, const int pVPH, const glm::mat4& pTransform) const { diff --git a/src/shaders/marker2d_vs.glsl b/src/shaders/marker2d_vs.glsl index cb8d8080..ed7b6028 100644 --- a/src/shaders/marker2d_vs.glsl +++ b/src/shaders/marker2d_vs.glsl @@ -1,10 +1,13 @@ #version 330 uniform mat4 transform; +uniform bool isPVROn; +uniform float psize; in vec2 point; in vec3 color; in float alpha; +in float pointsize; out vec4 pervcol; @@ -12,4 +15,5 @@ void main(void) { pervcol = vec4(color, alpha); gl_Position = transform * vec4(point.xy, 0, 1); + gl_PointSize = isPVROn ? pointsize : psize; } diff --git a/src/shaders/plot3_vs.glsl b/src/shaders/plot3_vs.glsl index ae497301..057860ba 100644 --- a/src/shaders/plot3_vs.glsl +++ b/src/shaders/plot3_vs.glsl @@ -1,10 +1,13 @@ #version 330 uniform mat4 transform; +uniform bool isPVROn; +uniform float psize; in vec3 point; in vec3 color; in float alpha; +in float pointsize; out vec4 hpoint; out vec4 pervcol; @@ -14,4 +17,5 @@ void main(void) hpoint = vec4(point.xyz,1); pervcol = vec4(color, alpha); gl_Position = transform * hpoint; + gl_PointSize = isPVROn ? pointsize : psize; } From a32199c06543cae45ec4a87c5d807dc7ea395a91 Mon Sep 17 00:00:00 2001 From: pradeep Date: Mon, 1 Feb 2016 23:13:29 +0530 Subject: [PATCH 17/61] Fixed alpha blending * Fixes in fragment shaders * Disabled depth test for alpha blending --- src/histogram.cpp | 3 +++ src/image.cpp | 3 +++ src/plot.cpp | 4 ++-- src/shaders/histogram_fs.glsl | 2 +- src/shaders/marker_fs.glsl | 4 +--- src/shaders/plot3_fs.glsl | 2 +- src/surface.cpp | 4 ++++ 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/histogram.cpp b/src/histogram.cpp index 20cc0238..d0f7b45d 100644 --- a/src/histogram.cpp +++ b/src/histogram.cpp @@ -142,6 +142,8 @@ void hist_impl::render(const int pWindowId, glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glScissor(pX, pY, pVPW, pVPH); glUseProgram(mProgram); @@ -162,6 +164,7 @@ void hist_impl::render(const int pWindowId, glUseProgram(0); glDisable(GL_SCISSOR_TEST); glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); CheckGL("End hist_impl::render"); } diff --git a/src/image.cpp b/src/image.cpp index bd4787ce..c640a604 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -157,6 +157,8 @@ void image_impl::render(const int pWindowId, glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_DEPTH_TEST); + glUseProgram(mProgram); glUniform1i(mNumCIndex, mFormatSize); @@ -189,6 +191,7 @@ void image_impl::render(const int pWindowId, // ubind the shader program glUseProgram(0); glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); CheckGL("End image_impl::render"); } diff --git a/src/plot.cpp b/src/plot.cpp index e9c466b0..44b5346d 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -206,7 +206,7 @@ void plot_impl::render(const int pWindowId, glEnable(GL_SCISSOR_TEST); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); + glDisable(GL_DEPTH_TEST); glm::mat4 mvp(1.0); this->computeTransformMat(mvp, pTransform, pX, pY, pVPW, pVPH); @@ -247,7 +247,7 @@ void plot_impl::render(const int pWindowId, } glDisable(GL_BLEND); - glDepthMask(GL_TRUE); + glEnable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); CheckGL("End plot_impl::render"); } diff --git a/src/shaders/histogram_fs.glsl b/src/shaders/histogram_fs.glsl index 8fb7f892..095d50a3 100644 --- a/src/shaders/histogram_fs.glsl +++ b/src/shaders/histogram_fs.glsl @@ -10,5 +10,5 @@ out vec4 outColor; void main(void) { float a = isPVAOn ? pervcol.w : 1.0; - outColor = isPVCOn ? vec4(pervcol.xyz, a) : barColor; + outColor = vec4(isPVCOn ? pervcol.xyz : barColor.xyz, isPVAOn ? pervcol.w : 1.0); } diff --git a/src/shaders/marker_fs.glsl b/src/shaders/marker_fs.glsl index 7aad8f7a..ac43f122 100644 --- a/src/shaders/marker_fs.glsl +++ b/src/shaders/marker_fs.glsl @@ -44,10 +44,8 @@ void main(void) in_bounds = true; } - float a = isPVAOn ? pervcol.w : 1.0; - if(!in_bounds) discard; else - outColor = isPVCOn ? vec4(pervcol.xyz, a) : marker_color; + outColor = vec4(isPVCOn ? pervcol.xyz : marker_color.xyz, isPVAOn ? pervcol.w : 1.0); } diff --git a/src/shaders/plot3_fs.glsl b/src/shaders/plot3_fs.glsl index b2918a2b..a7d827d7 100644 --- a/src/shaders/plot3_fs.glsl +++ b/src/shaders/plot3_fs.glsl @@ -28,5 +28,5 @@ void main(void) if(nin_bounds) discard; else - outColor = isPVCOn ? vec4(pervcol.xyz, a) : vec4(hsv2rgb(vec3(height, 1, 1)),1); + outColor = isPVCOn ? vec4(pervcol.xyz, a) : vec4(hsv2rgb(vec3(height, 1, 1)),a); } diff --git a/src/surface.cpp b/src/surface.cpp index 0d21be3a..8a284735 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -236,10 +236,14 @@ void surface_impl::render(const int pWindowId, CheckGL("Begin surface_impl::render"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_DEPTH_TEST); + glm::mat4 mvp(1.0); computeTransformMat(mvp, pModel); renderGraph(pWindowId, mvp); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); CheckGL("End surface_impl::render"); } From b2d5a6c6ae5748c68ed8f80f47cc8926dddd0b9c Mon Sep 17 00:00:00 2001 From: pradeep Date: Tue, 2 Feb 2016 00:39:59 +0530 Subject: [PATCH 18/61] changes in marker shader equations to draw solid markers --- src/shaders/marker_fs.glsl | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/shaders/marker_fs.glsl b/src/shaders/marker_fs.glsl index ac43f122..0e7cc068 100644 --- a/src/shaders/marker_fs.glsl +++ b/src/shaders/marker_fs.glsl @@ -10,35 +10,38 @@ out vec4 outColor; void main(void) { - float dist = sqrt( (gl_PointCoord.x - 0.5) * (gl_PointCoord.x-0.5) + (gl_PointCoord.y-0.5) * (gl_PointCoord.y-0.5) ); + vec2 coords = gl_PointCoord; + float dist = sqrt((coords.x - 0.5)*(coords.x-0.5) + (coords.y-0.5)*(coords.y-0.5)); bool in_bounds; + switch(marker_type) { case 1: - in_bounds = dist < 0.3; + in_bounds = dist<0.1; break; case 2: - in_bounds = ( (dist > 0.3) && (dist<0.5) ); + in_bounds = dist<0.5; break; case 3: - in_bounds = ((gl_PointCoord.x < 0.15) || (gl_PointCoord.x > 0.85)) || - ((gl_PointCoord.y < 0.15) || (gl_PointCoord.y > 0.85)); + in_bounds = ((coords.x > 0.15) || (coords.x < 0.85)) || + ((coords.y > 0.15) || (coords.y < 0.85)); break; case 4: - in_bounds = (2*(gl_PointCoord.x - 0.25) - (gl_PointCoord.y + 0.5) < 0) && (2*(gl_PointCoord.x - 0.25) + (gl_PointCoord.y + 0.5) > 1); + in_bounds = (2*(coords.x - 0.25) - (coords.y + 0.5) < 0) && + (2*(coords.x - 0.25) + (coords.y + 0.5) > 1); break; case 5: - in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.13 || - abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.13 ; + in_bounds = abs((coords.x - 0.5) + (coords.y - 0.5) ) < 0.13 || + abs((coords.x - 0.5) - (coords.y - 0.5) ) < 0.13 ; break; case 6: - in_bounds = abs((gl_PointCoord.x - 0.5)) < 0.07 || - abs((gl_PointCoord.y - 0.5)) < 0.07; + in_bounds = abs((coords.x - 0.5)) < 0.07 || + abs((coords.y - 0.5)) < 0.07; break; case 7: - in_bounds = abs((gl_PointCoord.x - 0.5) + (gl_PointCoord.y - 0.5) ) < 0.07 || - abs((gl_PointCoord.x - 0.5) - (gl_PointCoord.y - 0.5) ) < 0.07 || - abs((gl_PointCoord.x - 0.5)) < 0.07 || - abs((gl_PointCoord.y - 0.5)) < 0.07; + in_bounds = abs((coords.x - 0.5) + (coords.y - 0.5) ) < 0.07 || + abs((coords.x - 0.5) - (coords.y - 0.5) ) < 0.07 || + abs((coords.x - 0.5)) < 0.07 || + abs((coords.y - 0.5)) < 0.07; break; default: in_bounds = true; From 14fb507b26c06a66aaaab77d486aa0b000b62c0a Mon Sep 17 00:00:00 2001 From: pradeep Date: Tue, 2 Feb 2016 00:41:15 +0530 Subject: [PATCH 19/61] bubble chart example for cpu backend this example also demonstrates transparency and multiple plot rendering --- examples/cpu/bubblechart.cpp | 126 +++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 examples/cpu/bubblechart.cpp diff --git a/examples/cpu/bubblechart.cpp b/examples/cpu/bubblechart.cpp new file mode 100644 index 00000000..a46757fc --- /dev/null +++ b/examples/cpu/bubblechart.cpp @@ -0,0 +1,126 @@ +/******************************************************* + * Copyright (c) 2015-2019, ArrayFire + * All rights reserved. + * + * This file is distributed under 3-clause BSD license. + * The complete license agreement can be obtained at: + * http://arrayfire.com/licenses/BSD-3-Clause + ********************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const unsigned DIMX = 1000; +const unsigned DIMY = 800; + +const float FRANGE_START = 0.f; +const float FRANGE_END = 2.f * 3.1415926f; + +using namespace std; +void map_range_to_vec_vbo(float range_start, float range_end, float dx, + std::vector &vec, + float (*map) (float)) +{ + if(range_start > range_end && dx > 0) return; + for(float i=range_start; i < range_end; i+=dx){ + vec.push_back(i); + vec.push_back((*map)(i)); + } +} + +int main(void) +{ + std::vector cosData; + std::vector tanData; + + map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, cosData, &cosf); + map_range_to_vec_vbo(FRANGE_START, FRANGE_END, 0.1f, tanData, &tanf); + + std::random_device r; + + std::default_random_engine e1(r()); + std::mt19937_64 gen(r()); + + std::uniform_int_distribution uDist(20, 80); + std::uniform_real_distribution cDist(0.2, 0.6); + std::uniform_real_distribution fDist(0.4, 0.6); + + auto clr = std::bind(cDist, gen); + auto rnd = std::bind(uDist, e1); + auto alp = std::bind(fDist, gen); + + std::vector colors(3*tanData.size()); + std::vector alphas(tanData.size()); + std::vector radii(tanData.size()); + + std::generate(colors.begin(), colors.end(), clr); + std::generate(radii.begin(), radii.end(), rnd); + std::generate(alphas.begin(), alphas.end(), alp); + + /* + * First Forge call should be a window creation call + * so that necessary OpenGL context is created for any + * other fg::* object to be created successfully + */ + fg::Window wnd(DIMX, DIMY, "Plotting Demo"); + wnd.makeCurrent(); + /* create an font object and load necessary font + * and later pass it on to window object so that + * it can be used for rendering text */ + fg::Font fnt; +#ifdef OS_WIN + fnt.loadSystemFont("Calibri", 32); +#else + fnt.loadSystemFont("Vera", 32); +#endif + wnd.setFont(&fnt); + + fg::Chart chart(fg::FG_2D); + chart.setAxesLimits(FRANGE_START, FRANGE_END, -1.1f, 1.1f); + + /* Create several plot objects which creates the necessary + * vertex buffer objects to hold the different plot types + */ + fg::Plot plt1 = chart.plot(cosData.size()/2, fg::f32, + fg::FG_LINE, fg::FG_TRIANGLE); //or specify a specific plot type + fg::Plot plt2 = chart.plot(tanData.size()/2, fg::f32, + fg::FG_LINE, fg::FG_CIRCLE); //last parameter specifies marker shape + + /* Set plot colors */ + plt1.setColor(fg::FG_RED); + plt2.setColor(fg::FG_GREEN); //use a forge predefined color + /* Set plot legends */ + plt1.setLegend("Cosine"); + plt2.setLegend("Tangent"); + /* set plot global marker size */ + plt1.setMarkerSize(20); + /* copy your data into the opengl buffer object exposed by + * fg::Plot class and then proceed to rendering. + * To help the users with copying the data from compute + * memory to display memory, Forge provides copy headers + * along with the library to help with this task + */ + fg::copy(plt1.vertices(), plt1.verticesSize(), cosData.data()); + fg::copy(plt2.vertices(), plt2.verticesSize(), tanData.data()); + + /* update color value for tan graph */ + fg::copy(plt2.colors(), plt2.colorsSize(), colors.data()); + /* update alpha values for tan graph */ + fg::copy(plt2.alphas(), plt2.alphasSize(), alphas.data()); + /* update marker sizes for tan graph markers */ + fg::copy(plt2.markers(), plt2.markersSize(), radii.data()); + + do { + wnd.draw(chart); + } while(!wnd.close()); + + return 0; +} From 9c175b11c3ac827d799b99054ea6acd70d55bc1f Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 3 Feb 2016 09:53:37 +0530 Subject: [PATCH 20/61] Removed stray printfs used for debugging --- examples/cuda/histogram.cu | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/cuda/histogram.cu b/examples/cuda/histogram.cu index 797b3c4e..0abb3e66 100644 --- a/examples/cuda/histogram.cu +++ b/examples/cuda/histogram.cu @@ -105,9 +105,7 @@ int main(void) */ hist.setColor(fg::FG_YELLOW); - printf("1\n"); CUDA_ERROR_CHECK(cudaMalloc((void**)&dev_out, IMG_SIZE)); - printf("2\n"); CUDA_ERROR_CHECK(cudaMalloc((void**)&hist_out, NBINS * sizeof(int))); CUDA_ERROR_CHECK(cudaMalloc((void**)&hist_colors, 3*NBINS * sizeof(float))); From 53875c1c39d9638c3193f72d345a7b21612ff95e Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 3 Feb 2016 09:59:50 +0530 Subject: [PATCH 21/61] Moved y-axis title in 2d plots to left finally :) --- src/chart.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chart.cpp b/src/chart.cpp index a3260fd9..55b67017 100644 --- a/src/chart.cpp +++ b/src/chart.cpp @@ -339,7 +339,7 @@ void chart2d_impl::generateTickLabels() maxYLabelWidth = std::max(maxYLabelWidth, temp.length()); } - mLeftMargin = std::max((int)maxYLabelWidth, mLeftMargin); + mLeftMargin = std::max((int)maxYLabelWidth, mLeftMargin)+2*CHART2D_FONT_SIZE; /* push tick points for x axis */ mXText.push_back(toString(xmid)); @@ -430,7 +430,7 @@ void chart2d_impl::render(const int pWindowId, /* render chart axes titles */ if (!mYTitle.empty()) { glm::vec4 res = trans * glm::vec4(-1.0f, 0.0f, 0.0f, 1.0f); - pos[0] = w - CHART2D_FONT_SIZE; + pos[0] = 2; pos[1] = h*(res.y+1.0f)/2.0f; fonter->render(pWindowId, pos, WHITE, mYTitle.c_str(), CHART2D_FONT_SIZE, true); } From 95825ce2d10ce19a9b62e508dd915134d259cfa1 Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 3 Feb 2016 14:49:40 +0530 Subject: [PATCH 22/61] Corrected window names in examples --- examples/cpu/bubblechart.cpp | 2 +- examples/opencl/plotting.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cpu/bubblechart.cpp b/examples/cpu/bubblechart.cpp index a46757fc..c03d5943 100644 --- a/examples/cpu/bubblechart.cpp +++ b/examples/cpu/bubblechart.cpp @@ -70,7 +70,7 @@ int main(void) * so that necessary OpenGL context is created for any * other fg::* object to be created successfully */ - fg::Window wnd(DIMX, DIMY, "Plotting Demo"); + fg::Window wnd(DIMX, DIMY, "Bubble chart with Transparency Demo"); wnd.makeCurrent(); /* create an font object and load necessary font * and later pass it on to window object so that diff --git a/examples/opencl/plotting.cpp b/examples/opencl/plotting.cpp index c32e86df..60d823e9 100644 --- a/examples/opencl/plotting.cpp +++ b/examples/opencl/plotting.cpp @@ -83,7 +83,7 @@ int main(void) * so that necessary OpenGL context is created for any * other fg::* object to be created successfully */ - fg::Window wnd(DIMX, DIMY, "Fractal Demo"); + fg::Window wnd(DIMX, DIMY, "Plotting Demo"); wnd.makeCurrent(); /* create an font object and load necessary font * and later pass it on to window object so that From 12e367bace297460a57057b5e4e49db4ab7144e5 Mon Sep 17 00:00:00 2001 From: pradeep Date: Wed, 3 Feb 2016 15:11:40 +0530 Subject: [PATCH 23/61] Updated README with sample images of renderables --- README.md | 7 +++++++ docs/images/bubble.png | Bin 0 -> 43205 bytes docs/images/hist.png | Bin 0 -> 222879 bytes docs/images/image.png | Bin 0 -> 17776 bytes docs/images/plot.png | Bin 0 -> 35466 bytes docs/images/plot31.png | Bin 0 -> 41343 bytes docs/images/plot32.png | Bin 0 -> 57415 bytes docs/images/surface.png | Bin 0 -> 82678 bytes 8 files changed, 7 insertions(+) create mode 100644 docs/images/bubble.png create mode 100644 docs/images/hist.png create mode 100644 docs/images/image.png create mode 100644 docs/images/plot.png create mode 100644 docs/images/plot31.png create mode 100644 docs/images/plot32.png create mode 100644 docs/images/surface.png diff --git a/README.md b/README.md index 0b1de898..2ced05ae 100644 --- a/README.md +++ b/README.md @@ -17,3 +17,10 @@ A prototype of the OpenGL interop library that can be used with ArrayFire. The g * On `Linux` and `OS X`, [fontconfig](http://www.freedesktop.org/wiki/Software/fontconfig/) is required. Above dependencies are available through package managers on most of the Unix/Linux based distributions. We have provided an option in `CMake` for `Forge` to build it's own internal `freetype` version if you choose to not install it on your machine. + +### Sample Images +| | | +|-----|-----| +| Image | 2D Plot | +| 3d Plot | Rotated 3d Plot | +| histogram | Surface | diff --git a/docs/images/bubble.png b/docs/images/bubble.png new file mode 100644 index 0000000000000000000000000000000000000000..33d5d4ecd2127e4947b573cf1c605f22f4ec0818 GIT binary patch literal 43205 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

pd3G*)tK&T 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 P_D`rC*Q7jCGWPu+oip?< literal 0 HcmV?d00001 diff --git a/docs/images/hist.png b/docs/images/hist.png new file mode 100644 index 0000000000000000000000000000000000000000..040bc5f0fe9c5694d23c558ce547381d8245b816 GIT binary patch literal 222879 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

dRbfwCFi1_Y70! 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 %IDhgI4>CTjGj}JP6G{hlSmfLeX=zVEdG9 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�^wK5{5HG`^ITimvQMt8 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 literal 0 HcmV?d00001 diff --git a/docs/images/plot.png b/docs/images/plot.png new file mode 100644 index 0000000000000000000000000000000000000000..f239e7ebae9bd899e220cc68f9afeb7bfa91b08f GIT binary patch literal 35466 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%0Au zTDI;ijlLoQffaTL!o&qhIk{Jjygbd2@r?xq1y^_0MxvW0I6U=&OZ2$6fVbe{ zqd8e(YXNut`GLSn0aBz7+Vk$*3j%@Y9dP3?=dvNEy`8mi?fLJVxy!C2LE^igYu7%c zN3ykanH5}q{H`VA(Ma{?`wn+6rKy|d1yQTip({7}be285B+{AK8CqHU;`g>qd3n!? zx;jKQuqi}6GBJT9;nth8Zaq{A-9%{f|6cA=Du&kk04OfpcfI`k;gXn%iHUsJm3_%U zvGov?^)1cKDA;)S2m~s@AW#6!1keq9@^W%{AR>hqfy&^nKw5{H=h-XN?dI;%?jF)G zBeY9@`u&(*a7)CsIv~hm-4`1#HMR=PUO}Z9)`Oov8vF&qDGyrOmV``=$cC!*pGV!y~LLi9RO1JG8FU0htk-oCvb41F;%(0v@R?8`Hb>}id@nOnQY8q;Nl zjy9RQfbn1pfNKY79T)gwZrThF=H=yqNt3jwx=UQ(A?MIX5h%sDd zS!Cb(-*FGmd(I;s#XaL~Vq4i0lao;w2g+UYQ6yj)>AAD1v0e$iwE)fNniC|kU)OwW z6_co2%?rLABZ}qz*d4H`GDo{$ii#)*fB_skq-fcBd3m$ja>WEt%ha_L-@ zEnWZV?@?*J-|^?t`>g!wQ{3z{gWVTuIeGc4zgwW`{jPAsZ@}^Z&8UPe_EE+kO$+E9 zmOzqE0dzeJ?$ZP4k7;$U@6d6&snHrAgCNsKk52hH8-mziVx<=pM-%ognug$@u=et> zjXlr}M5wQ9?EZBtQg+hDQr)Y;FJE~7f`tF@8JhEn)GQaguaW7$jX(H|>Rj(j$uFyg zUu51sAf-_!3y_qYeqH_G*dky&2{_(#67-fP% zt1`Xf2xM|$!^3br`W`Jd4bBu#L(tr+OvSO3?^np=h0rI>Fs$;-33Pf7U1Hr} z^paq1ZuE8IR~J}V^u2O4vcf<(A~x~smrO7yP2OAPlzbicSO5y~!AQ@T?URt=29j!z zp<8S8@7r{HVibiSyf9usp(G(gO}Z(ZM=MP+Ry|FT0dD=t0@{xq270^4Wko9Ool?Zr zTj*UtxbH{+h7Y~1igK+4Sa$&2P7UU>`zeXO1W0KO(8kb!Py)Ro*`tRG01r8Nf;daM zJE{(s@-iQ|$@kD>WQVO&H19#F0=gEs6#2Y4Wf4%MHBKm2vm3+#b5Q=+mBo0Dv9186 zeIG<(INjlChkjHWwTh}!avQd|b&HzkJnBDmUw)ODpgUkI&(G2Ab+lgNZ?37ZoZS|m z9Vh>*Bj2|trT~SzlpQ~lIV+QXEtCPHw$%JSntr!UBmw8GyvubeZGGhBbn^xgdwGE; z%x1^1f9^PTHy^EInf>=|hfzgMAje;uX z_-c0y%B|IVqaX_~3V{*daK!Yt#u$;YL96CI zUF(R~o(CjXGD|YMk0<>`rFzh^+Zp~{mLLFt@EbBgSixNfpn*!XJT{j* zP_7#l_MK!?REK|dU`S{hCXvG5CIgqHLO5bZ;0`PVqAm!sS;TlR131)hO`4455OND} zDW)6W8sb#V0LC|{+t?t+=SJI9YK!l#CxKKAdK(Ggi;AS=6zZT_Il~X9FjbjR0A