--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/OHP3D.cpp Wed Dec 27 01:50:38 2023 +0100
@@ -0,0 +1,694 @@
+/**
+ * OHP3D
+ * Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <iostream>
+#include <iomanip>
+#include <string>
+#include <charconv>
+#include <memory>
+#include <functional>
+#include <sstream>
+#include <vector>
+
+#include "x11.h"
+#include "opengl.h"
+#include "EPoll.h"
+#include "Logger.h"
+#include "MappedFile.h"
+#include "ImageLoader.h"
+#include "Texture.h"
+#include "Shader.h"
+#include "Program.h"
+#include "FileMonitor.h"
+#include "XAttrs.h"
+
+#include "OHP3D.h"
+
+class OHP3D::Impl {
+public:
+
+ struct {
+ GLint aVertexXYZ = -2;
+ GLint aTextureXY = -2;
+
+ GLint fColor = -2;
+
+ GLint uModel = -2;
+ GLint uView = -2;
+ GLint uProjection = -2;
+ GLint uTexture = -2;
+ GLint uTextureScale = -2;
+ } ProgAttr;
+
+ struct {
+ float yaw = -90.f;
+ float pitch = 0.f;
+ float roll = 0.f;
+ float fov = 45.0f;
+ glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
+ glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
+ glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
+
+ void adjustFov(float diff) {
+ fov += diff;
+ if (fov < 1.0f) fov = 1.0f;
+ else if (fov > 120.0f) fov = 120.0f;
+ std::cerr << "field of view: " << fov << " °" << std::endl;
+ }
+
+ void moveForward(const float cameraSpeed) {
+ cameraPos += cameraSpeed * cameraFront;
+ }
+
+ void moveBackward(const float cameraSpeed) {
+ cameraPos -= cameraSpeed * cameraFront;
+ }
+
+ void moveLeft(const float cameraSpeed) {
+ cameraPos -= glm::normalize(
+ glm::cross(cameraFront, cameraUp)) * cameraSpeed;
+ }
+
+ void moveRight(const float cameraSpeed) {
+ cameraPos += glm::normalize(
+ glm::cross(cameraFront, cameraUp)) * cameraSpeed;
+ }
+
+ void moveUp(const float cameraSpeed) {
+ cameraPos += cameraSpeed * glm::normalize(cameraUp);
+ }
+
+ void moveDown(const float cameraSpeed) {
+ cameraPos -= cameraSpeed * glm::normalize(cameraUp);
+ }
+
+ void updateCameraFrontAndUp() {
+ std::cerr << "--- updateCameraFrontAndUp() --------" << std::endl;
+ dump("pitch, yaw, roll", glm::vec3(pitch, yaw, roll));
+ dump("cameraPos", cameraPos);
+ dump("cameraFront", cameraFront);
+ const auto pitchR = glm::radians(pitch); // around X axis
+ const auto yawR = glm::radians(yaw); // around Y axis
+ const auto rollR = glm::radians(roll); // around Z axis
+
+ cameraFront.x = cos(pitchR) * cos(yawR);
+ cameraFront.y = sin(pitchR);
+ cameraFront.z = cos(pitchR) * sin(yawR);
+ cameraFront = glm::normalize(cameraFront);
+ dump("cameraFront", cameraFront);
+ dump("cameraUp", cameraUp);
+
+ // TODO: review ROLL rotation and default angle
+ glm::mat4 rollMatrix = glm::rotate(
+ glm::mat4(1.0f), rollR, cameraFront);
+ cameraUp = glm::mat3(rollMatrix) * glm::vec3(0., 1., 0.);
+ dump("cameraUp", cameraUp);
+ std::cerr << "-------------------------------------" << std::endl;
+ }
+
+ void limitPitch() {
+ if (pitch > +89.0f) pitch = +89.0f;
+ if (pitch < -89.0f) pitch = -89.0f;
+ }
+
+ void turnLeft(const float angleSpeed) {
+ yaw -= angleSpeed;
+ updateCameraFrontAndUp();
+ }
+
+ void turnRight(const float angleSpeed) {
+ yaw += angleSpeed;
+ updateCameraFrontAndUp();
+ }
+
+ void turnUp(const float angleSpeed) {
+ pitch += angleSpeed;
+ limitPitch();
+ updateCameraFrontAndUp();
+ }
+
+ void turnDown(const float angleSpeed) {
+ pitch -= angleSpeed;
+ limitPitch();
+ updateCameraFrontAndUp();
+ }
+
+ void rollLeft(const float angleSpeed) {
+ roll += angleSpeed;
+ updateCameraFrontAndUp();
+ }
+
+ void rollRight(const float angleSpeed) {
+ roll -= angleSpeed;
+ updateCameraFrontAndUp();
+ }
+
+ } initialCtx, ctx;
+
+ Display* dpy;
+ Window win;
+ XVisualInfo* vi;
+ GLXContext glc;
+
+ FileMonitor fileMonitor;
+ std::vector<WatchedFile> watchedFiles;
+ ImageLoader imageLoader;
+ std::vector<std::shared_ptr<Shader>> shaders;
+ std::shared_ptr<Program> shaderProgram;
+ std::vector<std::shared_ptr<Texture>> textures;
+
+ Configuration cfg;
+ std::ostream& logOutput = std::cerr;
+
+ Impl(Configuration cfg) : cfg(cfg) {
+ }
+
+ void run();
+ void clear();
+ void runShaders();
+ Window getRootWindow(Window defaultValue);
+ void log(LogLevel level, std::string message);
+ int setNonBlocking(int fd);
+ void loadVertices();
+ void parametrizeTexture(std::shared_ptr<Texture> tex);
+ bool reloadTexture(const std::string& fileName);
+ void loadTextures();
+ void loadShaders();
+ void updateVariableLocations();
+ bool reloadShader(const std::string& fileName);
+ void setTitle(const std::string& suffix = "");
+ static const std::string getDefaultFile(const std::string& relativePath);
+
+};
+
+OHP3D::OHP3D(const Configuration& configuration) :
+impl(new Impl(configuration)) {
+}
+
+OHP3D::~OHP3D() {
+ impl->textures.clear();
+ impl->shaders.clear();
+ impl->shaderProgram = nullptr;
+ XFree(impl->vi);
+ glXMakeCurrent(impl->dpy, None, NULL);
+ glXDestroyContext(impl->dpy, impl->glc);
+ XDestroyWindow(impl->dpy, impl->win);
+ XCloseDisplay(impl->dpy);
+ delete impl;
+ // std::cerr << "~OHP3D()" << std::endl;
+}
+
+void OHP3D::Impl::setTitle(const std::string& suffix) {
+ std::stringstream title;
+ title << "OHP3D";
+ if (suffix.size()) title << ": " << suffix.c_str();
+ XStoreName(dpy, win, title.str().c_str());
+ XFlush(dpy);
+}
+
+void OHP3D::run() {
+ impl->run();
+}
+
+void OHP3D::Impl::run() {
+ dpy = XOpenDisplay(NULL);
+
+ if (dpy == NULL) throw std::logic_error("Unable to connect to X server");
+
+ GLint att[] = {GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None};
+ vi = glXChooseVisual(dpy, 0, att);
+ Window root = DefaultRootWindow(dpy);
+ Window parent = cfg.rootWindow ? cfg.rootWindow : root;
+
+ XSetWindowAttributes swa;
+ swa.colormap = XCreateColormap(dpy, parent, vi->visual, AllocNone);
+ swa.event_mask = ExposureMask | KeyPressMask | PointerMotionMask
+ | ButtonPressMask
+ | StructureNotifyMask;
+
+ bool full = false;
+ unsigned int width = 1600;
+ unsigned int height = 1200;
+ if (parent != root) {
+ XWindowAttributes parentAttr;
+ XGetWindowAttributes(dpy, parent, &parentAttr);
+ width = parentAttr.width;
+ height = parentAttr.height;
+ }
+
+ win = XCreateWindow(
+ dpy, parent, 0, 0, width, height, 0,
+ vi->depth, InputOutput, vi->visual,
+ CWColormap | CWEventMask, &swa);
+
+ XMapWindow(dpy, win);
+ setTitle();
+ setX11PID(dpy, win);
+ // XSetWindowBackground(dpy, win, 0) vs. glClearColor()
+
+ glc = glXCreateContext(dpy, vi, NULL, GL_TRUE);
+ glXMakeCurrent(dpy, win, glc);
+
+ glEnable(GL_DEPTH_TEST);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ clear();
+ glXSwapBuffers(dpy, win);
+
+
+ // Load GLSL shaders:
+ loadShaders();
+ loadTextures();
+ loadVertices();
+
+ auto toggleFullscreen = [&]() {
+ full = setFullscreen(dpy, win, !full);
+ };
+
+ auto resetView = [&]() {
+ ctx = initialCtx;
+ ctx.updateCameraFrontAndUp();
+ };
+
+ // root can reize our window
+ // or we can listen to root resize and then resize our window ourselves
+ bool listenToRootResizes = true;
+ if (listenToRootResizes) XSelectInput(dpy, parent, StructureNotifyMask);
+
+ bool keepRunningX11 = true;
+ int x11fd = XConnectionNumber(dpy);
+ EPoll epoll;
+ epoll.add(x11fd);
+ epoll.add(fileMonitor.getFD());
+ try {
+ epoll.add(setNonBlocking(STDIN_FILENO));
+ } catch (const EPoll::Exception& e) {
+ logOutput << "Will not monitor events on STDIN: " << e.what() << "\n";
+ }
+
+ // rended the 3D scene even before the first event:
+ runShaders();
+ glXSwapBuffers(dpy, win);
+
+ for (XEvent xev; keepRunningX11;) {
+ int epollEventCount = epoll.wait();
+ //std::cout << "trace: epoll.wait() = " << epollEventCount << std::endl;
+ for (int epollEvent = 0; epollEvent < epollEventCount; epollEvent++) {
+ bool redraw = false;
+ if (epoll[epollEvent].data.fd == x11fd) {
+ if (!XPending(dpy)) {
+ // otherwise STDIN events are held until the first X11 event
+ logOutput << "trace: no pending X11 event" << std::endl;
+ break;
+ }
+process_x11_event:
+ XWindowAttributes gwa;
+ XNextEvent(dpy, &xev);
+
+ if (xev.type == Expose) {
+ std::cout << "XEvent: Expose" << std::endl;
+ XGetWindowAttributes(dpy, win, &gwa);
+ glViewport(0, 0, gwa.width, gwa.height);
+ redraw = true;
+ } else if (xev.type == KeyPress) {
+ DecodedKey key = decodeKeycode(dpy, xev.xkey.keycode);
+ std::cout << "XEvent: KeyPress:"
+ << " keycode=" << key.code
+ << " key=" << key.name
+ << std::endl;
+
+ const float cSp = 0.05f; // camera speed
+ const float aSp = 5.f; // angle speed
+
+ if (key.matches(XK_q, XK_Escape)) keepRunningX11 = false;
+ else if (key.matches(XK_Left, XK_s)) ctx.turnLeft(aSp);
+ else if (key.matches(XK_Right, XK_f)) ctx.turnRight(aSp);
+ else if (key.matches(XK_Up, XK_e)) ctx.moveForward(cSp);
+ else if (key.matches(XK_Down, XK_d)) ctx.moveBackward(cSp);
+ else if (key.matches(XK_w)) ctx.rollLeft(aSp);
+ else if (key.matches(XK_r)) ctx.rollRight(aSp);
+ else if (key.matches(XK_t)) ctx.turnUp(aSp);
+ else if (key.matches(XK_g)) ctx.turnDown(aSp);
+ else if (key.matches(XK_m)) ctx.moveLeft(cSp);
+ else if (key.matches(XK_comma)) ctx.moveRight(cSp);
+ else if (key.matches(XK_l)) ctx.moveUp(cSp);
+ else if (key.matches(XK_period)) ctx.moveDown(cSp);
+ else if (key.matches(XK_j)) ctx.moveLeft(cSp * 5);
+ else if (key.matches(XK_k)) ctx.moveRight(cSp * 5);
+ else if (key.matches(XK_u)) ctx.moveLeft(cSp * 10);
+ else if (key.matches(XK_i)) ctx.moveRight(cSp * 10);
+ else if (key.matches(XK_x)) resetView();
+ else if (key.matches(XK_F11, XK_y)) toggleFullscreen();
+ redraw = true;
+ } else if (xev.type == ButtonPress) {
+ std::cout << "XEvent: ButtonPress:"
+ << " button=" << xev.xbutton.button
+ << std::endl;
+ if (xev.xbutton.button == 1);
+ else if (xev.xbutton.button == 4) ctx.adjustFov(-1.0);
+ else if (xev.xbutton.button == 5) ctx.adjustFov(+1.0);
+ else if (xev.xbutton.button == 8) resetView();
+ else if (xev.xbutton.button == 9) keepRunningX11 = false;
+ redraw = true;
+ } else if (xev.type == MotionNotify) {
+ // printCursorInfo(xev.xmotion);
+ } else if (xev.type == ConfigureNotify) {
+ std::cout << "XEvent: ConfigureNotify:"
+ << " window=" << xev.xconfigure.window
+ << " height=" << xev.xconfigure.height
+ << " width=" << xev.xconfigure.width
+ << std::endl;
+ if (listenToRootResizes
+ && xev.xconfigure.window == parent) {
+ XResizeWindow(dpy, win,
+ xev.xconfigure.width, xev.xconfigure.height);
+ }
+ } else if (xev.type == UnmapNotify) {
+ std::cout << "XEvent: UnmapNotify" << std::endl;
+ } else if (xev.type == DestroyNotify) {
+ std::cout << "XEvent: DestroyNotify → finish" << std::endl;
+ break;
+ } else {
+ std::cout << "XEvent: type=" << xev.type << std::endl;
+ }
+ if (XPending(dpy)) goto process_x11_event;
+ } else if (epoll[epollEvent].data.fd == STDIN_FILENO) {
+ int epollFD = epoll[epollEvent].data.fd;
+ logOutput << "other event: fd=" << epollFD << " data=";
+ for (char ch; read(epollFD, &ch, 1) > 0;) {
+ std::stringstream msg;
+ msg
+ << std::hex
+ << std::setfill('0')
+ << std::setw(2)
+ << (int) ch;
+ logOutput << msg.str();
+ }
+ logOutput << std::endl;
+
+ } else if (epoll[epollEvent].data.fd == fileMonitor.getFD()) {
+ std::cout << "FileMonitor event:" << std::endl;
+ for (FileEvent fe; fileMonitor.readEvent(fe);) {
+ logOutput << " "
+ << " file=" << fe.fileName
+ << " mask=" << fe.mask
+ << std::endl;
+ try {
+ redraw |= reloadTexture(fe.fileName);
+ redraw |= reloadShader(fe.fileName);
+ setTitle();
+ } catch (const std::exception& e) {
+ setTitle("[ERROR]");
+ logOutput << "error while reloading '"
+ << fe.fileName.c_str()
+ << "': " << e.what() << std::endl;
+ }
+ }
+ } else {
+ logOutput
+ << "error: event on an unexpected FD: "
+ << epoll[epollEvent].data.fd
+ << std::endl;
+ }
+
+ if (redraw) {
+ runShaders();
+ glXSwapBuffers(dpy, win);
+ }
+ }
+ }
+}
+
+void OHP3D::Impl::clear() {
+ glClearColor(
+ (cfg.backgroundColor >> 16 & 0xFF) / 256.,
+ (cfg.backgroundColor >> 8 & 0xFF) / 256.,
+ (cfg.backgroundColor & 0xFF) / 256.,
+ 1.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+void OHP3D::Impl::runShaders() {
+ shaderProgram->use();
+ checkError(&std::cerr);
+
+ clear();
+
+ GLint viewport[4];
+ glGetIntegerv(GL_VIEWPORT, viewport);
+ GLfloat width = viewport[2];
+ GLfloat height = viewport[3];
+
+ glm::mat4 projection = glm::perspective(
+ glm::radians(ctx.fov),
+ width / height,
+ 0.1f, 100.0f);
+ glUniformMatrix4fv(ProgAttr.uProjection, 1, GL_FALSE, &projection[0][0]);
+
+ glm::mat4 view = glm::lookAt(
+ ctx.cameraPos,
+ ctx.cameraPos + ctx.cameraFront,
+ ctx.cameraUp);
+ glUniformMatrix4fv(ProgAttr.uView, 1, GL_FALSE, &view[0][0]);
+
+ // glBindVertexArray(vao);
+
+ glm::mat4 model = glm::mat4(1.0f); // identity matrix
+ glUniformMatrix4fv(ProgAttr.uModel, 1, GL_FALSE, &model[0][0]);
+
+ // TODO: draw a rectangle for each texture
+ glUniform1f(ProgAttr.uTextureScale, textures[0]->getScale());
+
+ glDrawArrays(GL_TRIANGLES, 0, 2 * 3); // see loadVertices()
+ std::cerr << "GLSL: glDrawArrays()" << std::endl;
+}
+
+void OHP3D::Impl::log(LogLevel level, std::string message) {
+ ::log(logOutput, level, message);
+}
+
+int OHP3D::Impl::setNonBlocking(int fd) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+ return fd;
+}
+
+void OHP3D::Impl::loadVertices() {
+ for (int i = 0; i < textures.size(); i++) {
+ std::shared_ptr<Texture> tex = textures[i];
+ // TODO: draw a rectangle for each texture
+ GLfloat ratio = tex->getRatio();
+ const std::vector<GLfloat> vertices = {
+ // Vertex XYZ Texture XY
+ -0.80f * ratio, +0.80f, +0.0, /**/ 0.0, 0.0,
+ +0.80f * ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
+ -0.80f * ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
+
+ -0.80f * ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
+ +0.80f * ratio, -0.80f, +0.0, /**/ 1.0, 1.0,
+ +0.80f * ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
+
+ // see glDrawArrays(), where we set start offset and count
+ };
+
+ // Vertex data:
+ glVertexAttribPointer(ProgAttr.aVertexXYZ, 3, // vertex items
+ GL_FLOAT, GL_FALSE, 5 * sizeof (float),
+ (void*) 0);
+ glEnableVertexAttribArray(ProgAttr.aVertexXYZ);
+
+ // Texture positions:
+ glVertexAttribPointer(ProgAttr.aTextureXY, 2, // texture items
+ GL_FLOAT, GL_FALSE, 5 * sizeof (float),
+ (void*) (3 * sizeof (float)));
+ glEnableVertexAttribArray(ProgAttr.aTextureXY);
+
+ glBufferData(GL_ARRAY_BUFFER,
+ vertices.size() * sizeof (vertices[0]),
+ vertices.data(),
+ GL_STATIC_DRAW);
+ // GL_STATIC_DRAW:
+ // The vertex data will be uploaded once
+ // and drawn many times(e.g. the world).
+ // GL_DYNAMIC_DRAW:
+ // The vertex data will be created once, changed from
+ // time to time, but drawn many times more than that.
+ // GL_STREAM_DRAW:
+ // The vertex data will be uploaded once and drawn once.
+
+ // see also glBindBuffer(GL_ARRAY_BUFFER, vbo); where we set current VBO
+ }
+}
+
+const std::string
+OHP3D::Impl::getDefaultFile(const std::string& relativePath) {
+ const char* envName = "OHP3D_DATA_DIR";
+ const char* envValue = ::getenv(envName);
+ if (envValue) {
+ return std::string(envValue) + "/" + relativePath;
+ } else {
+ throw std::invalid_argument(std::string("Configure $") + envName
+ + " in order to use defaults"
+ " or specify textures and shaders as parameters");
+ }
+}
+
+void OHP3D::Impl::parametrizeTexture(std::shared_ptr<Texture> tex) {
+ XAttrs xa(tex->getFileName());
+ std::string magf = xa["ohp3d.texture.mag-filter"];
+ std::string minf = xa["ohp3d.texture.min-filter"];
+ std::string scale = xa["ohp3d.texture.scale"];
+
+ auto GLT2D = GL_TEXTURE_2D;
+ auto MAG = GL_TEXTURE_MAG_FILTER;
+ auto MIN = GL_TEXTURE_MIN_FILTER;
+
+ if (magf == "linear") glTexParameteri(GLT2D, MAG, GL_LINEAR);
+ else if (magf == "nearest") glTexParameteri(GLT2D, MAG, GL_NEAREST);
+
+ if (minf == "linear") glTexParameteri(GLT2D, MIN, GL_LINEAR);
+ else if (minf == "nearest") glTexParameteri(GLT2D, MIN, GL_NEAREST);
+
+ if (scale.size()) {
+ float sc;
+ if (std::from_chars(scale.data(), scale.data() + scale.size(), sc).ec
+ == std::errc{}) tex->setScale(sc);
+ else std::cerr << "Invalid texture scale value - expecting float\n";
+ // tex->setScale(std::stof(scale)); // locale-dependent (. vs ,)
+ }
+}
+
+void OHP3D::Impl::loadTextures() {
+ // Load default texture if there is no configured:
+ if (cfg.textures.empty())
+ cfg.textures.push_back({getDefaultFile("textures/default.png")});
+
+ for (const Configuration::Texture& tex : cfg.textures) {
+ std::shared_ptr<ImageLoader::ImageBuffer>
+ img(imageLoader.loadImage(MappedFile(tex.fileName)));
+ textures.push_back(std::make_shared<Texture>(
+ img->width, img->height, *img, tex.fileName));
+ parametrizeTexture(textures.back());
+ // static const uint32_t watchMask = IN_CLOSE_WRITE | IN_ATTRIB;
+ // watchedFiles.push_back(fileMonitor.watch(tex.fileName, watchMask));
+ watchedFiles.push_back(fileMonitor.watch(tex.fileName));
+ // TODO: review texture loading and binding
+ // works even without this - default texture
+ // glUniform1i(..., ...);
+ // checkError(&std::cerr);
+ }
+}
+
+bool OHP3D::Impl::reloadTexture(const std::string& fileName) {
+ for (std::shared_ptr<Texture> tex : textures) {
+ if (tex->getFileName() == fileName) {
+ std::shared_ptr<ImageLoader::ImageBuffer>
+ img(imageLoader.loadImage(MappedFile(fileName)));
+ tex->update(img->width, img->height, *img);
+ parametrizeTexture(tex);
+ loadVertices();
+ return true;
+ }
+ }
+ return false;
+}
+
+void OHP3D::Impl::loadShaders() {
+ // Vertex Array Object (VAO)
+ GLuint vao;
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+ // VAO - something like context for bound data/variables
+ // We can switch multiple VAOs. VAO can contain multiple VBOs.
+ // what-are-vertex-array-objects
+
+ // Vertex Buffer Object (VBO):
+ GLuint vbo;
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+
+ {
+ // Load default shaders if there are no configured:
+ int vc = 0;
+ int fc = 0;
+ auto& ss = cfg.shaders;
+ for (const auto& s : ss) if (s.type == "vertex") vc++;
+ for (const auto& s : ss) if (s.type == "fragment") fc++;
+ auto& d = getDefaultFile;
+ if (vc == 0) ss.push_back({d("shaders/default.vert"), "vertex"});
+ if (fc == 0) ss.push_back({d("shaders/default.frag"), "fragment"});
+ }
+
+ shaderProgram = std::make_shared<Program>();
+
+ // glBindFragDataLocation(program, 0, "outColor");
+ // glBindAttribLocation(program, LOC.input, "vertices");
+
+ for (const Configuration::Shader definition : cfg.shaders) {
+ Shader::Type type;
+ std::string fileName = definition.fileName;
+ if (definition.type == "fragment") type = Shader::Type::FRAGMENT;
+ else if (definition.type == "vertex") type = Shader::Type::VERTEX;
+ else throw std::invalid_argument("unsupported shader type");
+
+ MappedFile file(fileName);
+ std::shared_ptr<Shader> shader = std::make_shared<Shader>(
+ type, file, fileName);
+
+ shaderProgram->attachShader(*shader.get());
+ shaders.push_back(shader);
+ watchedFiles.push_back(fileMonitor.watch(fileName));
+ std::cerr << "GLSL loaded: " << fileName.c_str() << std::endl;
+ // We may detach and delete shaders,
+ // but our shaders are small, so we keep them for later reloading.
+ }
+
+ shaderProgram->link();
+ updateVariableLocations();
+ // listVariables(program);
+ std::cerr << "GLSL shader count: " << shaders.size() << std::endl;
+}
+
+void OHP3D::Impl::updateVariableLocations() {
+ // GLSL compiler does very efficient / aggressive optimization.
+ // Attributes and uniforms that are not used in the shader are deleted.
+ // And even if we e.g. read color from a texture and overwrite it,
+ // the variable is still deleted and considered „inactive“.
+ // Functions glGetAttribLocation() and glGetUniformLocation() return -1.
+ ProgAttr.aVertexXYZ = shaderProgram->getAttribLocation("aVertexXYZ");
+ ProgAttr.aTextureXY = shaderProgram->getAttribLocation("aTextureXY");
+ ProgAttr.uModel = shaderProgram->getUniformLocation("uModel");
+ ProgAttr.uView = shaderProgram->getUniformLocation("uView");
+ ProgAttr.uProjection = shaderProgram->getUniformLocation("uProjection");
+ ProgAttr.uTexture = shaderProgram->getUniformLocation("uTexture");
+ ProgAttr.uTextureScale = shaderProgram->getUniformLocation("uTextureScale");
+ ProgAttr.fColor = shaderProgram->getFragDataLocation("fColor");
+ shaderProgram->bindFragDataLocation("fColor", ProgAttr.fColor);
+}
+
+bool OHP3D::Impl::reloadShader(const std::string& fileName) {
+ for (auto shader : shaders) {
+ if (shader->getFileName() == fileName) {
+ shader->update(MappedFile(fileName));
+ shaderProgram->link();
+ updateVariableLocations();
+ return true;
+ }
+ }
+ return false;
+}