0
|
1 |
/**
|
|
2 |
* ShaderShark
|
|
3 |
* Copyright © 2023 František Kučera (Frantovo.cz, GlobalCode.info)
|
|
4 |
*
|
|
5 |
* This program is free software: you can redistribute it and/or modify
|
|
6 |
* it under the terms of the GNU General Public License as published by
|
|
7 |
* the Free Software Foundation, version 3 of the License.
|
|
8 |
*
|
|
9 |
* This program is distributed in the hope that it will be useful,
|
|
10 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12 |
* GNU General Public License for more details.
|
|
13 |
*
|
|
14 |
* You should have received a copy of the GNU General Public License
|
|
15 |
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16 |
*/
|
|
17 |
|
|
18 |
#include "Shark.h"
|
|
19 |
|
|
20 |
Shark::Shark(const Configuration& configuration) :
|
|
21 |
cfg(configuration) {
|
|
22 |
}
|
|
23 |
|
|
24 |
Shark::~Shark() {
|
|
25 |
}
|
|
26 |
|
|
27 |
void Shark::run() {
|
|
28 |
Display* dpy = XOpenDisplay(NULL);
|
|
29 |
|
|
30 |
if (dpy == NULL) throw std::logic_error("Unable to connect to X server");
|
|
31 |
|
|
32 |
GLint att[] = {GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None};
|
|
33 |
XVisualInfo* vi = glXChooseVisual(dpy, 0, att);
|
|
34 |
Window root = DefaultRootWindow(dpy);
|
|
35 |
Window parent = cfg.rootWindow ? cfg.rootWindow : root;
|
|
36 |
|
|
37 |
XSetWindowAttributes swa;
|
|
38 |
swa.colormap = XCreateColormap(dpy, parent, vi->visual, AllocNone);
|
|
39 |
swa.event_mask = ExposureMask | KeyPressMask | PointerMotionMask
|
|
40 |
| ButtonPressMask
|
|
41 |
| StructureNotifyMask;
|
|
42 |
|
|
43 |
bool full = false;
|
|
44 |
unsigned int width = 1600;
|
|
45 |
unsigned int height = 1200;
|
|
46 |
if (parent != root) {
|
|
47 |
XWindowAttributes parentAttr;
|
|
48 |
XGetWindowAttributes(dpy, parent, &parentAttr);
|
|
49 |
width = parentAttr.width;
|
|
50 |
height = parentAttr.height;
|
|
51 |
}
|
|
52 |
|
|
53 |
Window win = XCreateWindow(
|
|
54 |
dpy, parent, 0, 0, width, height, 0,
|
|
55 |
vi->depth, InputOutput, vi->visual,
|
|
56 |
CWColormap | CWEventMask, &swa);
|
|
57 |
|
|
58 |
XMapWindow(dpy, win);
|
|
59 |
XStoreName(dpy, win, "ShaderShark");
|
|
60 |
setX11PID(dpy, win);
|
|
61 |
// XSetWindowBackground(dpy, win, 0) vs. glClearColor()
|
|
62 |
|
|
63 |
GLXContext glc = glXCreateContext(dpy, vi, NULL, GL_TRUE);
|
|
64 |
glXMakeCurrent(dpy, win, glc);
|
|
65 |
|
|
66 |
// Load GLSL shaders:
|
|
67 |
GLuint shaderProgram = loadShaders();
|
|
68 |
loadVertices();
|
|
69 |
loadTextures(shaderProgram);
|
|
70 |
|
|
71 |
auto toggleFullscreen = [&]() {
|
|
72 |
full = setFullscreen(dpy, win, !full);
|
|
73 |
};
|
|
74 |
|
|
75 |
auto resetView = [&]() {
|
|
76 |
ctx = initialCtx;
|
|
77 |
ctx.updateCameraFrontAndUp();
|
|
78 |
};
|
|
79 |
|
|
80 |
// root can reize our window
|
|
81 |
// or we can listen to root resize and then resize our window ourselves
|
|
82 |
bool listenToRootResizes = true;
|
|
83 |
if (listenToRootResizes) XSelectInput(dpy, parent, StructureNotifyMask);
|
|
84 |
|
|
85 |
bool keepRunningX11 = true;
|
|
86 |
int x11fd = XConnectionNumber(dpy);
|
|
87 |
EPoll epoll;
|
|
88 |
epoll.add(x11fd);
|
|
89 |
try {
|
|
90 |
epoll.add(setNonBlocking(STDIN_FILENO));
|
|
91 |
} catch (const EPoll::Exception& e) {
|
|
92 |
logOutput << "Will not monitor events on STDIN: " << e.what() << "\n";
|
|
93 |
}
|
|
94 |
|
|
95 |
// rended the 3D scene even before the first event:
|
|
96 |
runShaders(shaderProgram);
|
|
97 |
glXSwapBuffers(dpy, win);
|
|
98 |
|
|
99 |
for (XEvent xev; keepRunningX11;) {
|
|
100 |
int epollEventCount = epoll.wait();
|
|
101 |
//std::cout << "trace: epoll.wait() = " << epollEventCount << std::endl;
|
|
102 |
for (int epollEvent = 0; epollEvent < epollEventCount; epollEvent++) {
|
|
103 |
if (epoll[epollEvent].data.fd == x11fd) {
|
|
104 |
if (!XPending(dpy)) {
|
|
105 |
// otherwise STDIN events are held until the first X11 event
|
|
106 |
logOutput << "trace: no pending X11 event" << std::endl;
|
|
107 |
break;
|
|
108 |
}
|
|
109 |
XWindowAttributes gwa;
|
|
110 |
XNextEvent(dpy, &xev);
|
|
111 |
bool redraw = false;
|
|
112 |
|
|
113 |
if (xev.type == Expose) {
|
|
114 |
std::cout << "XEvent: Expose" << std::endl;
|
|
115 |
XGetWindowAttributes(dpy, win, &gwa);
|
|
116 |
glViewport(0, 0, gwa.width, gwa.height);
|
|
117 |
redraw = true;
|
|
118 |
} else if (xev.type == KeyPress) {
|
|
119 |
DecodedKey key = decodeKeycode(dpy, xev.xkey.keycode);
|
|
120 |
std::cout << "XEvent: KeyPress:"
|
|
121 |
<< " keycode=" << key.code
|
|
122 |
<< " key=" << key.name
|
|
123 |
<< std::endl;
|
|
124 |
|
|
125 |
const float cSp = 0.05f; // camera speed
|
|
126 |
const float aSp = 5.f; // angle speed
|
|
127 |
|
|
128 |
if (key.matches(XK_q, XK_Escape)) keepRunningX11 = false;
|
|
129 |
else if (key.matches(XK_Left, XK_s)) ctx.turnLeft(aSp);
|
|
130 |
else if (key.matches(XK_Right, XK_f)) ctx.turnRight(aSp);
|
|
131 |
else if (key.matches(XK_Up, XK_e)) ctx.moveForward(cSp);
|
|
132 |
else if (key.matches(XK_Down, XK_d)) ctx.moveBackward(cSp);
|
|
133 |
else if (key.matches(XK_w)) ctx.rollLeft(aSp);
|
|
134 |
else if (key.matches(XK_r)) ctx.rollRight(aSp);
|
|
135 |
else if (key.matches(XK_t)) ctx.turnUp(aSp);
|
|
136 |
else if (key.matches(XK_g)) ctx.turnDown(aSp);
|
|
137 |
else if (key.matches(XK_m)) ctx.moveLeft(cSp);
|
|
138 |
else if (key.matches(XK_comma)) ctx.moveRight(cSp);
|
|
139 |
else if (key.matches(XK_l)) ctx.moveUp(cSp);
|
|
140 |
else if (key.matches(XK_period)) ctx.moveDown(cSp);
|
|
141 |
else if (key.matches(XK_j)) ctx.moveLeft(cSp * 5);
|
|
142 |
else if (key.matches(XK_k)) ctx.moveRight(cSp * 5);
|
|
143 |
else if (key.matches(XK_u)) ctx.moveLeft(cSp * 10);
|
|
144 |
else if (key.matches(XK_i)) ctx.moveRight(cSp * 10);
|
|
145 |
else if (key.matches(XK_x)) resetView();
|
|
146 |
else if (key.matches(XK_F11, XK_y)) toggleFullscreen();
|
|
147 |
redraw = true;
|
|
148 |
} else if (xev.type == ButtonPress) {
|
|
149 |
std::cout << "XEvent: ButtonPress:"
|
|
150 |
<< " button=" << xev.xbutton.button
|
|
151 |
<< std::endl;
|
|
152 |
if (xev.xbutton.button == 1);
|
|
153 |
else if (xev.xbutton.button == 4) ctx.adjustFov(+1.0);
|
|
154 |
else if (xev.xbutton.button == 5) ctx.adjustFov(-1.0);
|
|
155 |
else if (xev.xbutton.button == 8) resetView();
|
|
156 |
else if (xev.xbutton.button == 9) keepRunningX11 = false;
|
|
157 |
redraw = true;
|
|
158 |
} else if (xev.type == MotionNotify) {
|
|
159 |
// printCursorInfo(xev.xmotion);
|
|
160 |
} else if (xev.type == ConfigureNotify) {
|
|
161 |
std::cout << "XEvent: ConfigureNotify:"
|
|
162 |
<< " window=" << xev.xconfigure.window
|
|
163 |
<< " height=" << xev.xconfigure.height
|
|
164 |
<< " width=" << xev.xconfigure.width
|
|
165 |
<< std::endl;
|
|
166 |
if (listenToRootResizes
|
|
167 |
&& xev.xconfigure.window == parent) {
|
|
168 |
XResizeWindow(dpy, win,
|
|
169 |
xev.xconfigure.width, xev.xconfigure.height);
|
|
170 |
}
|
|
171 |
} else if (xev.type == UnmapNotify) {
|
|
172 |
std::cout << "XEvent: UnmapNotify" << std::endl;
|
|
173 |
} else if (xev.type == DestroyNotify) {
|
|
174 |
std::cout << "XEvent: DestroyNotify → finish" << std::endl;
|
|
175 |
break;
|
|
176 |
} else {
|
|
177 |
std::cout << "XEvent: type=" << xev.type << std::endl;
|
|
178 |
}
|
|
179 |
|
|
180 |
if (redraw) {
|
|
181 |
runShaders(shaderProgram);
|
|
182 |
glXSwapBuffers(dpy, win);
|
|
183 |
}
|
|
184 |
} else if (epoll[epollEvent].data.fd == STDIN_FILENO) {
|
|
185 |
int epollFD = epoll[epollEvent].data.fd;
|
|
186 |
logOutput << "other event: fd=" << epollFD << " data=";
|
|
187 |
for (char ch; read(epollFD, &ch, 1) > 0;) {
|
|
188 |
std::stringstream msg;
|
|
189 |
msg
|
|
190 |
<< std::hex
|
|
191 |
<< std::setfill('0')
|
|
192 |
<< std::setw(2)
|
|
193 |
<< (int) ch;
|
|
194 |
logOutput << msg.str();
|
|
195 |
}
|
|
196 |
logOutput << std::endl;
|
|
197 |
|
|
198 |
} else {
|
|
199 |
logOutput
|
|
200 |
<< "error: event on an unexpected FD: "
|
|
201 |
<< epoll[epollEvent].data.fd
|
|
202 |
<< std::endl;
|
|
203 |
}
|
|
204 |
}
|
|
205 |
}
|
|
206 |
|
|
207 |
XFree(vi);
|
|
208 |
// for (auto page : pdfTextures) glDeleteTextures(1, &page.texture);
|
|
209 |
|
|
210 |
glXMakeCurrent(dpy, None, NULL);
|
|
211 |
glXDestroyContext(dpy, glc);
|
|
212 |
XDestroyWindow(dpy, win);
|
|
213 |
XCloseDisplay(dpy);
|
|
214 |
}
|
|
215 |
|
|
216 |
void Shark::runShaders(GLuint program) {
|
|
217 |
std::cerr << "GLSL: runShaders(" << program << ")" << std::endl;
|
|
218 |
std::cerr << "background color: " << cfg.backgroundColor << std::endl;
|
|
219 |
glUseProgram(program);
|
|
220 |
checkError(&std::cerr);
|
|
221 |
|
|
222 |
glClearColor(
|
|
223 |
(cfg.backgroundColor >> 16 & 0xFF) / 256.,
|
|
224 |
(cfg.backgroundColor >> 8 & 0xFF) / 256.,
|
|
225 |
(cfg.backgroundColor & 0xFF) / 256.,
|
|
226 |
1.0);
|
|
227 |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
228 |
|
|
229 |
GLint viewport[4];
|
|
230 |
glGetIntegerv(GL_VIEWPORT, viewport);
|
|
231 |
GLfloat width = viewport[2];
|
|
232 |
GLfloat height = viewport[3];
|
|
233 |
|
|
234 |
glm::mat4 projection = glm::perspective(
|
|
235 |
glm::radians(ctx.fov),
|
|
236 |
width / height,
|
|
237 |
0.1f, 100.0f);
|
|
238 |
glUniformMatrix4fv(ProgAttr.projection, 1, GL_FALSE, &projection[0][0]);
|
|
239 |
|
|
240 |
glm::mat4 view = glm::lookAt(
|
|
241 |
ctx.cameraPos,
|
|
242 |
ctx.cameraPos + ctx.cameraFront,
|
|
243 |
ctx.cameraUp);
|
|
244 |
glUniformMatrix4fv(ProgAttr.view, 1, GL_FALSE, &view[0][0]);
|
|
245 |
|
|
246 |
// glBindVertexArray(vao);
|
|
247 |
|
|
248 |
glm::mat4 model = glm::mat4(1.0f); // identity matrix
|
|
249 |
// model = glm::translate(model, glm::vec3(0., 0., 0.));
|
|
250 |
// float angle = 20.0f;
|
|
251 |
// glm::vec3 xxx = glm::vec3(1.0f, 0.3f, 0.5f);
|
|
252 |
// model = glm::rotate(model, glm::radians(angle), xxx);
|
|
253 |
glUniformMatrix4fv(ProgAttr.model, 1, GL_FALSE, &model[0][0]);
|
|
254 |
|
|
255 |
glDrawArrays(GL_TRIANGLES, 0, 2 * 3); // viz loadVertices() kde plníme data
|
|
256 |
std::cerr << "GLSL: glDrawArrays()" << std::endl;
|
|
257 |
}
|
|
258 |
|
|
259 |
void Shark::log(LogLevel level, std::string message) {
|
|
260 |
::log(logOutput, level, message);
|
|
261 |
}
|
|
262 |
|
|
263 |
int Shark::setNonBlocking(int fd) {
|
|
264 |
int flags = fcntl(fd, F_GETFL, 0);
|
|
265 |
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
266 |
return fd;
|
|
267 |
}
|
|
268 |
|
|
269 |
void Shark::loadVertices() {
|
|
270 |
const std::vector<GLfloat> vertices = {
|
|
271 |
// Vertex XYZ Texture XY
|
|
272 |
-0.80f * TEX.ratio, +0.80f, +0.0, /**/ 0.0, 0.0,
|
|
273 |
+0.80f * TEX.ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
|
|
274 |
-0.80f * TEX.ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
|
|
275 |
|
|
276 |
-0.80f * TEX.ratio, -0.80f, +0.0, /**/ 0.0, 1.0,
|
|
277 |
+0.80f * TEX.ratio, -0.80f, +0.0, /**/ 1.0, 1.0,
|
|
278 |
+0.80f * TEX.ratio, +0.80f, +0.0, /**/ 1.0, 0.0,
|
|
279 |
|
|
280 |
// viz glDrawArrays(), kde vybereme počátek a počet hodnot
|
|
281 |
};
|
|
282 |
|
|
283 |
// Vertex data:
|
|
284 |
glVertexAttribPointer(ProgAttr.vertexXYZ, 3, // vertex items
|
|
285 |
GL_FLOAT, GL_FALSE, 5 * sizeof (float),
|
|
286 |
(void*) 0);
|
|
287 |
glEnableVertexAttribArray(ProgAttr.vertexXYZ);
|
|
288 |
|
|
289 |
// Texture positions:
|
|
290 |
glVertexAttribPointer(ProgAttr.textureXY, 2, // texture items
|
|
291 |
GL_FLOAT, GL_FALSE, 5 * sizeof (float),
|
|
292 |
(void*) (3 * sizeof (float)));
|
|
293 |
glEnableVertexAttribArray(ProgAttr.textureXY);
|
|
294 |
|
|
295 |
glBufferData(GL_ARRAY_BUFFER,
|
|
296 |
vertices.size() * sizeof (vertices[0]),
|
|
297 |
vertices.data(),
|
|
298 |
GL_STATIC_DRAW);
|
|
299 |
// GL_STATIC_DRAW:
|
|
300 |
// The vertex data will be uploaded once
|
|
301 |
// and drawn many times(e.g. the world).
|
|
302 |
// GL_DYNAMIC_DRAW:
|
|
303 |
// The vertex data will be created once, changed from
|
|
304 |
// time to time, but drawn many times more than that.
|
|
305 |
// GL_STREAM_DRAW:
|
|
306 |
// The vertex data will be uploaded once and drawn once.
|
|
307 |
|
|
308 |
// see also glBindBuffer(GL_ARRAY_BUFFER, vbo); where we set current VBO
|
|
309 |
}
|
|
310 |
|
|
311 |
GLuint Shark::loadTexture(const std::string& fileName, int width, int height) {
|
|
312 |
MappedFile file(fileName);
|
|
313 |
if (file.getSize() == width * height * 4) {
|
|
314 |
GLuint textureID;
|
|
315 |
glGenTextures(1, &textureID);
|
|
316 |
glBindTexture(GL_TEXTURE_2D, textureID);
|
|
317 |
auto GLT2D = GL_TEXTURE_2D;
|
|
318 |
glTexImage2D(GLT2D, 0, GL_RGBA,
|
|
319 |
width, height,
|
|
320 |
0, GL_RGBA, GL_UNSIGNED_BYTE,
|
|
321 |
file.getData());
|
|
322 |
glTexParameteri(GLT2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
|
323 |
glTexParameteri(GLT2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
|
324 |
glTexParameteri(GLT2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
325 |
glTexParameteri(GLT2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
326 |
glGenerateMipmap(GLT2D);
|
|
327 |
std::cerr << "loadTexture(\"" << fileName.c_str() << "\", "
|
|
328 |
<< width << ", " << height << ") = " << textureID << std::endl;
|
|
329 |
checkError(&std::cerr);
|
|
330 |
return textureID;
|
|
331 |
} else {
|
|
332 |
throw std::invalid_argument("wrong texture file size");
|
|
333 |
}
|
|
334 |
}
|
|
335 |
|
|
336 |
void Shark::loadTextures(GLuint shaderProgram) {
|
|
337 |
for (const Configuration::Texture& tex : cfg.textures) {
|
|
338 |
GLuint jazz = loadTexture(tex.fileName, TEX.width, TEX.height);
|
|
339 |
// FIXME: decode PNG, JPEG etc. formats
|
|
340 |
// FIXME: read width and height from the file
|
|
341 |
// TODO: review texture loading and binding
|
|
342 |
// works even without this - default texture
|
|
343 |
// glUniform1i(ProgAttr.jazz, jazz);
|
|
344 |
// checkError(&std::cerr);
|
|
345 |
}
|
|
346 |
}
|
|
347 |
|
|
348 |
GLuint Shark::loadShaders() {
|
|
349 |
try {
|
|
350 |
// Vertex Array Object (VAO)
|
|
351 |
GLuint vao;
|
|
352 |
glGenVertexArrays(1, &vao);
|
|
353 |
glBindVertexArray(vao);
|
|
354 |
// VAO - something like context for bound data/variables
|
|
355 |
// We can switch multiple VAOs. VAO can contain multiple VBOs.
|
|
356 |
// See also https://stackoverflow.com/questions/11821336/
|
|
357 |
// what-are-vertex-array-objects
|
|
358 |
|
|
359 |
// Vertex Buffer Object (VBO):
|
|
360 |
GLuint vbo;
|
|
361 |
glGenBuffers(1, &vbo);
|
|
362 |
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
|
363 |
|
|
364 |
std::vector<std::string> fileNames = {
|
|
365 |
"shaders/first.vert",
|
|
366 |
"shaders/first.frag",
|
|
367 |
};
|
|
368 |
|
|
369 |
std::vector<GLuint> shaders;
|
|
370 |
|
|
371 |
GLuint program = glCreateProgram();
|
|
372 |
|
|
373 |
// glBindFragDataLocation(program, 0, "outColor");
|
|
374 |
// glBindAttribLocation(program, LOC.input, "vertices");
|
|
375 |
|
|
376 |
for (const std::string& fileName : fileNames) {
|
|
377 |
MappedFile file(fileName);
|
|
378 |
GLuint shader = glCreateShader(toShaderType(fileName));
|
|
379 |
auto fileData = file.getData();
|
|
380 |
GLint fileSize = file.getSize();
|
|
381 |
glShaderSource(shader, 1, &fileData, &fileSize);
|
|
382 |
glCompileShader(shader);
|
|
383 |
|
|
384 |
GLint compileStatus;
|
|
385 |
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
|
|
386 |
std::cerr << "GLSL shader compile status: "
|
|
387 |
<< compileStatus
|
|
388 |
<< (compileStatus == GL_TRUE ? " = OK" : " = ERROR")
|
|
389 |
<< std::endl;
|
|
390 |
|
|
391 |
if (compileStatus != GL_TRUE) {
|
|
392 |
char error[512];
|
|
393 |
glGetShaderInfoLog(shader, sizeof (error), NULL, error);
|
|
394 |
std::cerr << "GLSL shader error: " << error;
|
|
395 |
throw std::logic_error("GLSL: shader failed to compile");
|
|
396 |
}
|
|
397 |
|
|
398 |
glAttachShader(program, shader);
|
|
399 |
shaders.push_back(shader);
|
|
400 |
std::cerr << "GLSL loaded: " << fileName.c_str() << std::endl;
|
|
401 |
}
|
|
402 |
|
|
403 |
// GLSL compiler does very efficient / aggressive optimization.
|
|
404 |
// Attributes and uniforms that are not used in the shader are deleted.
|
|
405 |
// And even if we e.g. read color from a texture and overwrite it,
|
|
406 |
// the variable is still deleted and considered „inactive“.
|
|
407 |
// Functions glGetAttribLocation() and glGetUniformLocation() return -1.
|
|
408 |
|
|
409 |
glLinkProgram(program);
|
|
410 |
|
|
411 |
ProgAttr.vertexXYZ = glGetAttribLocation(program, "vertices");
|
|
412 |
ProgAttr.textureXY = glGetAttribLocation(program, "textureCoordinates");
|
|
413 |
ProgAttr.model = glGetUniformLocation(program, "model");
|
|
414 |
ProgAttr.view = glGetUniformLocation(program, "view");
|
|
415 |
ProgAttr.projection = glGetUniformLocation(program, "projection");
|
|
416 |
ProgAttr.jazz = glGetUniformLocation(program, "jazz");
|
|
417 |
ProgAttr.color = glGetFragDataLocation(program, "outColor");
|
|
418 |
glBindFragDataLocation(program, ProgAttr.color, "outColor");
|
|
419 |
// listVariables(program);
|
|
420 |
GLint linkStatus;
|
|
421 |
glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
|
|
422 |
if (linkStatus != GL_TRUE) {
|
|
423 |
char error[512];
|
|
424 |
glGetProgramInfoLog(program, sizeof (error), NULL, error);
|
|
425 |
std::cerr << "GLSL program error: " << error;
|
|
426 |
throw std::logic_error("GLSL: program failed to link");
|
|
427 |
}
|
|
428 |
std::cerr << "GLSL shader count: " << shaders.size() << std::endl;
|
|
429 |
|
|
430 |
return program;
|
|
431 |
} catch (const std::exception& e) {
|
|
432 |
std::cerr << "Error while loading shaders: " << e.what() << std::endl;
|
|
433 |
} catch (...) {
|
|
434 |
std::cerr << "Error while loading shaders: unknown" << std::endl;
|
|
435 |
}
|
|
436 |
throw std::logic_error("GLSL: loadShaders() failed");
|
|
437 |
}
|
|
438 |
|