diff -r 4cbd9c0beb4c -r dc3c102e1264 OHP3D.cpp --- /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 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 watchedFiles; + ImageLoader imageLoader; + std::vector> shaders; + std::shared_ptr shaderProgram; + std::vector> 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 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 tex = textures[i]; + // TODO: draw a rectangle for each texture + GLfloat ratio = tex->getRatio(); + const std::vector 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 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 + img(imageLoader.loadImage(MappedFile(tex.fileName))); + textures.push_back(std::make_shared( + 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 tex : textures) { + if (tex->getFileName() == fileName) { + std::shared_ptr + 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(); + + // 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 = std::make_shared( + 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; +}