blob: b10a1ded81face32ec564d724f9c23eaf4d8251b [file] [log] [blame]
#include "frc971/analysis/plotting/webgl2_plotter.h"
#include <assert.h>
#include <stdlib.h>
#include <iostream>
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
namespace frc971 {
namespace plotting {
namespace {
// Shader and program construction taken from examples at
// https://github.com/emscripten-core/emscripten/blob/incoming/tests/webgl2_draw_packed_triangle.c
GLuint compile_shader(GLenum shaderType, const char *src) {
GLuint shader = glCreateShader(shaderType);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
GLint isCompiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
if (!isCompiled) {
GLint maxLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
char *buf = (char *)malloc(maxLength + 1);
glGetShaderInfoLog(shader, maxLength, &maxLength, buf);
printf("%s\n", buf);
free(buf);
return 0;
}
return shader;
}
GLuint create_program(GLuint vertexShader, GLuint fragmentShader,
GLuint attribute_location) {
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glBindAttribLocation(program, attribute_location, "apos");
glLinkProgram(program);
return program;
}
GLuint CreateLineProgram(GLuint attribute_location) {
// Create a shader program which will take in:
// -A series of points to plot
// -Scale/offset parameters for choosing how to zoom/pan
// -Point size/color information
//
// The vertex shader then takes in the series of points (apos) and
// transforms the points by the scale/offset to determine their on-screen
// position. We aren't doing any funny with 3D or perspective, so we leave
// the z and w components untouched.
//
// We set the color of the line in the fragment shader.
const char vertex_shader[] =
"#version 100\n"
"attribute vec2 apos;"
"uniform vec2 scale;"
"uniform vec2 offset;"
"uniform float point_size;"
"void main() {"
"gl_Position.xy = apos.xy * scale.xy + offset.xy;"
"gl_Position.z = 0.0;"
"gl_Position.w = 1.0;"
"gl_PointSize = point_size;"
"}";
GLuint vs = compile_shader(GL_VERTEX_SHADER, vertex_shader);
const char fragment_shader[] =
"#version 100\n"
"precision lowp float;"
"uniform vec4 color;"
"void main() {"
"gl_FragColor = color;"
"}";
GLuint fs = compile_shader(GL_FRAGMENT_SHADER, fragment_shader);
return create_program(vs, fs, attribute_location);
}
const Eigen::Vector2d Vector2dInfinity() {
return Eigen::Vector2d::Ones() * std::numeric_limits<double>::infinity();
}
} // namespace
class WebglLine : public Line {
public:
WebglLine(GLuint program, size_t buffer_size = 1000000)
: color_uniform_location_(glGetUniformLocation(program, "color")),
point_size_uniform_location_(
glGetUniformLocation(program, "point_size")),
line_size_(0) {}
virtual ~WebglLine() {}
void SetPoints(const std::vector<Eigen::Vector2d> &pts) override {
updated_ = true;
max_values_ = -Vector2dInfinity();
min_values_ = Vector2dInfinity();
line_size_ = 0;
buffer_.clear();
for (const auto &pt : pts) {
buffer_.push_back(pt.x());
buffer_.push_back(pt.y());
max_values_ = max_values_.cwiseMax(pt);
min_values_ = min_values_.cwiseMin(pt);
++line_size_;
}
}
void Draw() override {
updated_ = false;
if (buffer_.empty()) {
return;
}
// TODO(james): Flushing and rewriting the buffer on every line draw seems
// like it should be inefficient, but in practice it seems to actually be
// fine for the amounts of data that we are dealing with (i.e., doing a few
// tens of MB of copies at the most is not actually that expensive).
glBufferData(GL_ARRAY_BUFFER, buffer_.size() * sizeof(float),
buffer_.data(), GL_STATIC_DRAW);
glUniform4f(color_uniform_location_, color_.r, color_.g, color_.b, 1.0);
glUniform1f(point_size_uniform_location_, point_size_);
if (point_size_ != 0) {
glDrawArrays(GL_POINTS, 0, line_size_);
}
if (line_width_ != 0) {
glDrawArrays(GL_LINE_STRIP, 0, line_size_);
}
assert(GL_NO_ERROR == glGetError() && "glDrawArray failed");
}
void SetColor(const Color &color) override {
updated_ = true;
color_ = color;
}
Eigen::Vector2d MaxValues() const override { return max_values_; }
Eigen::Vector2d MinValues() const override { return min_values_; }
void SetLineWidth(const float width) override {
updated_ = true;
line_width_ = width;
}
void SetPointSize(const float point_size) override {
updated_ = true;
point_size_ = point_size;
}
bool HasUpdate() override { return updated_; }
private:
const GLuint color_uniform_location_;
const GLuint point_size_uniform_location_;
std::vector<float> buffer_;
size_t line_size_;
Color color_;
Eigen::Vector2d max_values_ = -Vector2dInfinity();
Eigen::Vector2d min_values_ = Vector2dInfinity();
float line_width_ = 1.0;
float point_size_ = 3.0;
bool updated_ = true;
};
WebglCanvasPlotter::WebglCanvasPlotter(const std::string &canvas_id,
GLuint attribute_location) {
EmscriptenWebGLContextAttributes attr;
emscripten_webgl_init_context_attributes(&attr);
assert(attr.antialias && "Antialiasing should be enabled by default.");
attr.majorVersion = 2;
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context("#canvas", &attr);
assert(ctx && "Failed to create WebGL2 context");
emscripten_webgl_make_context_current(ctx);
program_ = CreateLineProgram(attribute_location);
scale_uniform_location_ = glGetUniformLocation(program_, "scale");
offset_uniform_location_ = glGetUniformLocation(program_, "offset");
glGenBuffers(1, &gl_buffer_);
glUseProgram(program_);
glBindBuffer(GL_ARRAY_BUFFER, gl_buffer_);
glVertexAttribPointer(attribute_location, 2, GL_FLOAT, GL_FALSE, 8, 0);
glEnableVertexAttribArray(attribute_location);
zoom_rectangle_ = std::make_unique<WebglLine>(program_);
zoom_rectangle_->SetColor({.r = 1.0, .g = 1.0, .b = 1.0});
zoom_rectangle_->SetLineWidth(2.0);
zoom_rectangle_->SetPointSize(0.0);
}
Line *WebglCanvasPlotter::AddLine() {
lines_.push_back(std::make_unique<WebglLine>(program_));
return lines_.back().get();
}
void WebglCanvasPlotter::Undo() {
if (old_scales_.empty() || old_offsets_.empty()) {
return;
}
scale_ = old_scales_.back();
old_scales_.pop_back();
offset_ = old_offsets_.back();
old_offsets_.pop_back();
}
void WebglCanvasPlotter::RecordState() {
old_scales_.push_back(scale_);
old_offsets_ .push_back(offset_);
}
void WebglCanvasPlotter::SetScale(const Eigen::Vector2d &scale) {
scale_ = scale;
}
Eigen::Vector2d WebglCanvasPlotter::GetScale() const {
return scale_;
}
void WebglCanvasPlotter::SetOffset(const Eigen::Vector2d &offset) {
offset_ = offset;
}
Eigen::Vector2d WebglCanvasPlotter::GetOffset() const {
return offset_;
}
void WebglCanvasPlotter::Redraw() {
const bool scaling_update = last_scale_ != scale_ || last_offset_ != offset_;
bool data_update = zoom_rectangle_->HasUpdate();
for (const auto &line : lines_) {
data_update = line->HasUpdate() || data_update;
}
if (!scaling_update && !data_update) {
return;
}
glUseProgram(program_);
glBindBuffer(GL_ARRAY_BUFFER, gl_buffer_);
glUniform2f(scale_uniform_location_, scale_.x(), scale_.y());
glUniform2f(offset_uniform_location_, offset_.x(), offset_.y());
for (const auto &line : lines_) {
line->Draw();
}
zoom_rectangle_->Draw();
last_scale_ = scale_;
last_offset_ = offset_;
}
Eigen::Vector2d WebglCanvasPlotter::MaxValues() const {
Eigen::Vector2d max = -Vector2dInfinity();
for (const auto &line : lines_) {
max = max.cwiseMax(line->MaxValues());
}
return max;
}
Eigen::Vector2d WebglCanvasPlotter::MinValues() const {
Eigen::Vector2d min = Vector2dInfinity();
for (const auto &line : lines_) {
min = min.cwiseMin(line->MinValues());
}
return min;
}
void WebglCanvasPlotter::ClearZoomRectangle() {
zoom_rectangle_->SetPoints({});
}
void WebglCanvasPlotter::SetZoomRectangle(const Eigen::Vector2d &corner1,
const Eigen::Vector2d &corner2) {
zoom_rectangle_->SetPoints({corner1,
{corner1.x(), corner2.y()},
corner2,
{corner2.x(), corner1.y()},
corner1});
}
} // namespace plotting
} // namespace frc971