wolfhece 2.0.13__py3-none-any.whl → 2.0.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wolfhece/PyDraw.py +46 -11
- wolfhece/PyPalette.py +6 -0
- wolfhece/Results2DGPU.py +5 -1
- wolfhece/opengl/__init__.py +0 -0
- wolfhece/opengl/gl_utils.py +1544 -0
- wolfhece/opengl/py3d.py +1665 -0
- wolfhece/opengl/tile_packer.py +352 -0
- wolfhece/scenario/imposebc_void.py +6 -3
- wolfhece/scenario/update_void.py +6 -3
- wolfhece/shaders/fragment_shader_texture.glsl +12 -0
- wolfhece/shaders/geom_grid.glsl +32 -0
- wolfhece/shaders/quad_frag_shader.glsl +34 -0
- wolfhece/shaders/quad_geom_shader.glsl +234 -9
- wolfhece/shaders/quadpos_frag_shader.glsl +12 -0
- wolfhece/shaders/quadpos_geom_shader.glsl +76 -0
- wolfhece/shaders/simple_fragment_shader.glsl +4 -1
- wolfhece/shaders/simple_vertex_shader_mvp.glsl +13 -0
- wolfhece/shaders/vertex_shader_texture.glsl +15 -0
- wolfhece/wolf_array.py +52 -8
- wolfhece/wolfresults_2D.py +14 -12
- {wolfhece-2.0.13.dist-info → wolfhece-2.0.15.dist-info}/METADATA +2 -1
- {wolfhece-2.0.13.dist-info → wolfhece-2.0.15.dist-info}/RECORD +25 -14
- {wolfhece-2.0.13.dist-info → wolfhece-2.0.15.dist-info}/WHEEL +0 -0
- {wolfhece-2.0.13.dist-info → wolfhece-2.0.15.dist-info}/entry_points.txt +0 -0
- {wolfhece-2.0.13.dist-info → wolfhece-2.0.15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1544 @@
|
|
1
|
+
""" This module helps to upload shaders and textures in the GPU.
|
2
|
+
It also provides a lot of controls to ensure compatibility
|
3
|
+
of textures, samplers, uniforms types (because OpenGL is pretty
|
4
|
+
silent about them and incompatibilities lead to mindbending debugging)
|
5
|
+
It is very basic and tuned to our needs.
|
6
|
+
"""
|
7
|
+
import re
|
8
|
+
import os
|
9
|
+
import ctypes
|
10
|
+
import logging
|
11
|
+
from typing import Union
|
12
|
+
from pathlib import Path
|
13
|
+
# from traceback import print_stack
|
14
|
+
|
15
|
+
import numpy as np
|
16
|
+
from OpenGL.GL import (
|
17
|
+
GL_MAX_TEXTURE_IMAGE_UNITS,
|
18
|
+
glActiveTexture,
|
19
|
+
GL_COLOR_ATTACHMENT0,
|
20
|
+
GL_COLOR_ATTACHMENT1,
|
21
|
+
GL_COLOR_ATTACHMENT2,
|
22
|
+
GL_COLOR_ATTACHMENT3,
|
23
|
+
GL_COLOR_ATTACHMENT4,
|
24
|
+
GL_COLOR_ATTACHMENT5,
|
25
|
+
GL_COLOR_ATTACHMENT6,
|
26
|
+
GL_COLOR_ATTACHMENT7,
|
27
|
+
GL_COLOR_ATTACHMENT8,
|
28
|
+
GL_TEXTURE0,
|
29
|
+
GL_TEXTURE1,
|
30
|
+
GL_TEXTURE2,
|
31
|
+
GL_TEXTURE3,
|
32
|
+
GL_TEXTURE4,
|
33
|
+
GL_TEXTURE5,
|
34
|
+
GL_TEXTURE6,
|
35
|
+
GL_TEXTURE7,
|
36
|
+
GL_TEXTURE8,
|
37
|
+
GL_TEXTURE9,
|
38
|
+
GL_TEXTURE10,
|
39
|
+
GL_TEXTURE11,
|
40
|
+
GL_VERTEX_SHADER,
|
41
|
+
GL_FRAGMENT_SHADER,
|
42
|
+
GL_GEOMETRY_SHADER,
|
43
|
+
GL_COMPUTE_SHADER,
|
44
|
+
GL_MAX_VIEWPORT_DIMS,
|
45
|
+
glCreateShader,
|
46
|
+
glCompileShader,
|
47
|
+
glDeleteShader,
|
48
|
+
GL_COMPILE_STATUS,
|
49
|
+
glShaderSource,
|
50
|
+
glGetShaderiv,
|
51
|
+
glGetShaderInfoLog,
|
52
|
+
GL_FALSE,
|
53
|
+
GL_TRUE,
|
54
|
+
glDeleteProgram,
|
55
|
+
GL_TEXTURE_RECTANGLE,
|
56
|
+
GL_TEXTURE_2D,
|
57
|
+
glGenTextures,
|
58
|
+
glTexParameteri,
|
59
|
+
GL_TEXTURE_MAG_FILTER,
|
60
|
+
GL_NEAREST,
|
61
|
+
GL_TEXTURE_MIN_FILTER,
|
62
|
+
GL_NEAREST,
|
63
|
+
GL_TEXTURE_WRAP_S,
|
64
|
+
GL_CLAMP_TO_EDGE,
|
65
|
+
GL_TEXTURE_WRAP_T,
|
66
|
+
GL_CLAMP_TO_EDGE,
|
67
|
+
glBindTexture,
|
68
|
+
GL_R32F,
|
69
|
+
GL_RED,
|
70
|
+
GL_FLOAT,
|
71
|
+
glTexImage2D,
|
72
|
+
GL_RGB32F,
|
73
|
+
GL_RGB32UI,
|
74
|
+
GL_RGB,
|
75
|
+
GL_RGBA,
|
76
|
+
GL_RGBA_INTEGER,
|
77
|
+
GL_R8UI,
|
78
|
+
GL_R32UI,
|
79
|
+
GL_R32I,
|
80
|
+
GL_RG16UI,
|
81
|
+
GL_RGB16UI,
|
82
|
+
GL_RG_INTEGER,
|
83
|
+
GL_RGB_INTEGER,
|
84
|
+
GL_BGRA,
|
85
|
+
GL_RGBA8,
|
86
|
+
GL_RG32F,
|
87
|
+
GL_RG,
|
88
|
+
GL_RED_INTEGER,
|
89
|
+
GL_UNSIGNED_BYTE,
|
90
|
+
GL_UNSIGNED_INT,
|
91
|
+
GL_UNSIGNED_SHORT,
|
92
|
+
GL_SHORT,
|
93
|
+
GL_INT,
|
94
|
+
glBindFramebuffer,
|
95
|
+
GL_FRAMEBUFFER,
|
96
|
+
glGetError,
|
97
|
+
GL_NO_ERROR,
|
98
|
+
glReadBuffer,
|
99
|
+
glReadPixels,
|
100
|
+
glGetTexImage,
|
101
|
+
glGenFramebuffers,
|
102
|
+
glFramebufferTexture2D,
|
103
|
+
glDrawBuffers,
|
104
|
+
glCheckFramebufferStatus,
|
105
|
+
GL_FRAMEBUFFER_COMPLETE,
|
106
|
+
GL_NONE,
|
107
|
+
glCreateProgram,
|
108
|
+
glAttachShader,
|
109
|
+
glLinkProgram,
|
110
|
+
glGetProgramiv,
|
111
|
+
GL_LINK_STATUS,
|
112
|
+
glGetUniformLocation,
|
113
|
+
glUniform1f,
|
114
|
+
glUniform1ui,
|
115
|
+
glUniform1i,
|
116
|
+
glUniform2f,
|
117
|
+
glUniformMatrix4fv,
|
118
|
+
glGetIntegerv,
|
119
|
+
GL_MAJOR_VERSION,
|
120
|
+
GL_MINOR_VERSION,
|
121
|
+
glGetString,
|
122
|
+
GL_VERSION,
|
123
|
+
GL_VENDOR,
|
124
|
+
GL_SHADING_LANGUAGE_VERSION,
|
125
|
+
glGetInteger,
|
126
|
+
glClearTexImage,
|
127
|
+
glGenVertexArrays,
|
128
|
+
glBindVertexArray,
|
129
|
+
glBindBuffer,
|
130
|
+
GL_ARRAY_BUFFER,
|
131
|
+
glBufferData,
|
132
|
+
GL_STATIC_DRAW,
|
133
|
+
glGenBuffers,
|
134
|
+
glEnableVertexAttribArray,
|
135
|
+
glVertexAttribPointer,
|
136
|
+
glBindVertexArray,
|
137
|
+
glPixelStorei,
|
138
|
+
GL_UNPACK_ALIGNMENT,
|
139
|
+
GL_PACK_ALIGNMENT,
|
140
|
+
GL_CURRENT_PROGRAM
|
141
|
+
)
|
142
|
+
|
143
|
+
from OpenGL.GL import (
|
144
|
+
GL_DEPTH_ATTACHMENT,
|
145
|
+
glGenRenderbuffers,
|
146
|
+
glBindRenderbuffer,
|
147
|
+
GL_RENDERBUFFER,
|
148
|
+
glRenderbufferStorage,
|
149
|
+
GL_DEPTH_COMPONENT16,
|
150
|
+
glFramebufferRenderbuffer,
|
151
|
+
GL_DEPTH_COMPONENT32F,
|
152
|
+
GL_READ_FRAMEBUFFER,
|
153
|
+
glDeleteTextures,
|
154
|
+
GL_FRAMEBUFFER_BINDING,
|
155
|
+
)
|
156
|
+
|
157
|
+
from OpenGL.GL import GL_READ_ONLY, GL_RGBA32F, GL_RGBA32UI, glBindImageTexture
|
158
|
+
|
159
|
+
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
160
|
+
import pygame
|
161
|
+
|
162
|
+
# I use these arrays to avoid dubious computations such as
|
163
|
+
# "GL_COLOR_ATTACHMENT0 + n".
|
164
|
+
# FIXME Populate them in function of the GPU's maximum values.
|
165
|
+
|
166
|
+
GL_COLOR_ATTACHMENTS = [
|
167
|
+
GL_COLOR_ATTACHMENT0,
|
168
|
+
GL_COLOR_ATTACHMENT1,
|
169
|
+
GL_COLOR_ATTACHMENT2,
|
170
|
+
GL_COLOR_ATTACHMENT3,
|
171
|
+
GL_COLOR_ATTACHMENT4,
|
172
|
+
GL_COLOR_ATTACHMENT5,
|
173
|
+
GL_COLOR_ATTACHMENT6,
|
174
|
+
GL_COLOR_ATTACHMENT7,
|
175
|
+
GL_COLOR_ATTACHMENT8,
|
176
|
+
]
|
177
|
+
TEXTURE_UNITS = [
|
178
|
+
GL_TEXTURE0,
|
179
|
+
GL_TEXTURE1,
|
180
|
+
GL_TEXTURE2,
|
181
|
+
GL_TEXTURE3,
|
182
|
+
GL_TEXTURE4,
|
183
|
+
GL_TEXTURE5,
|
184
|
+
GL_TEXTURE6,
|
185
|
+
GL_TEXTURE7,
|
186
|
+
GL_TEXTURE8,
|
187
|
+
GL_TEXTURE9,
|
188
|
+
GL_TEXTURE10,
|
189
|
+
GL_TEXTURE11,
|
190
|
+
]
|
191
|
+
TEX_SAMPLERS_RE = re.compile(
|
192
|
+
r".*uniform\s+(sampler2DRect|usampler2DRect|isampler2DRect|image2D|image2DRect|uimage2DRect)\s+(\w+)\s*;"
|
193
|
+
)
|
194
|
+
IMAGE_UNIT_RE = re.compile(r"layout([,]+, binding = [0-9]+)")
|
195
|
+
|
196
|
+
|
197
|
+
def check_gl_error():
|
198
|
+
err = glGetError()
|
199
|
+
assert err == GL_NO_ERROR, f"GlError = {err}"
|
200
|
+
|
201
|
+
def rgb_to_rgba(t):
|
202
|
+
assert len(t.shape) == 3
|
203
|
+
assert t.shape[2] == 3
|
204
|
+
return np.pad(t, ((0, 0), (0, 0), (0, 1)))
|
205
|
+
|
206
|
+
def memory_aligned_byte_array(size, value):
|
207
|
+
# OpenGL wants 4-byts aligned values
|
208
|
+
# One can use:
|
209
|
+
# from OpenGL.GL import glPixelStorei, GL_UNPACK_ALIGNMENT
|
210
|
+
# glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
|
211
|
+
# but it's a global setting
|
212
|
+
|
213
|
+
# This code doesn't work...
|
214
|
+
logging.debug(f"memory_aligned_byte_array size={size} value={value}")
|
215
|
+
assert ctypes.alignment(ctypes.c_uint32) == 4
|
216
|
+
assert ctypes.alignment(ctypes.c_uint32) == glGetInteger(
|
217
|
+
GL_UNPACK_ALIGNMENT
|
218
|
+
), "We must align on OpenGL alignment"
|
219
|
+
v = value << 24 + value << 16 + value << 8 + value
|
220
|
+
padded_len = (size + 3) // 4
|
221
|
+
img_data = (ctypes.c_uint32 * padded_len)(*([v] * padded_len))
|
222
|
+
assert ctypes.addressof(img_data) % 4 == 0
|
223
|
+
# I bet the issue is that img_data being mapped only
|
224
|
+
# it may be gc'ed before we have a chance to send it
|
225
|
+
# to OpenGL...
|
226
|
+
img_data2 = (ctypes.c_uint8 * size).from_buffer(img_data)
|
227
|
+
logging.debug("memory_aligned_byte_array - done")
|
228
|
+
return img_data2
|
229
|
+
|
230
|
+
def _get_format_type_from_internal_format(internal_format):
|
231
|
+
if internal_format == GL_R32F:
|
232
|
+
format, type_ = GL_RED, GL_FLOAT
|
233
|
+
elif internal_format == GL_RGB32F:
|
234
|
+
format, type_ = GL_RGB, GL_FLOAT
|
235
|
+
elif internal_format == GL_RGBA32F:
|
236
|
+
format, type_ = GL_RGBA, GL_FLOAT
|
237
|
+
elif internal_format == GL_RGBA32UI:
|
238
|
+
format, type_ = GL_RGBA_INTEGER, GL_UNSIGNED_INT
|
239
|
+
elif internal_format == GL_R32UI:
|
240
|
+
format, type_ = GL_RED_INTEGER, GL_UNSIGNED_INT
|
241
|
+
# See https://stackoverflow.com/questions/59542891/opengl-integer-texture-raising-gl-invalid-value
|
242
|
+
# GL_RED_INTEGER is for unnormalized values (such as uint's)
|
243
|
+
elif internal_format == GL_R8UI:
|
244
|
+
format, type_ = GL_RED_INTEGER, GL_UNSIGNED_BYTE
|
245
|
+
elif internal_format == GL_R32I:
|
246
|
+
format, type_ = GL_RED_INTEGER, GL_INT
|
247
|
+
elif internal_format == GL_RG16UI:
|
248
|
+
# FIXME This doesn't work, but it should...
|
249
|
+
# pyopengl sayz : ValueError: Unrecognised image format: GL_RG_INTEGER
|
250
|
+
format, type_ = GL_RG_INTEGER, GL_UNSIGNED_SHORT
|
251
|
+
elif internal_format == GL_RGB16UI:
|
252
|
+
format, type_ = GL_RGB_INTEGER, GL_UNSIGNED_SHORT
|
253
|
+
elif internal_format == GL_RGB32UI:
|
254
|
+
format, type_ = GL_RGB_INTEGER, GL_UNSIGNED_INT
|
255
|
+
else:
|
256
|
+
raise Exception(f"Unsupported format {internal_format}")
|
257
|
+
|
258
|
+
return format, type_
|
259
|
+
|
260
|
+
def read_texture2(tex_id, desired_format, width:int = None, height:int = None) -> np.ndarray:
|
261
|
+
""" Read a texture `tex_id` out of the GPU and returns it as an array.
|
262
|
+
|
263
|
+
The desired_fromat is the one you want to get the texture in. Be aware that
|
264
|
+
some formats are not supported by python OpenGL (right now, we know of
|
265
|
+
RG16UI).
|
266
|
+
"""
|
267
|
+
format, type_ = _get_format_type_from_internal_format(desired_format)
|
268
|
+
glBindTexture(GL_TEXTURE_RECTANGLE, tex_id)
|
269
|
+
|
270
|
+
# No need to know the height/width.
|
271
|
+
# npa will be "bytes".
|
272
|
+
npa = glGetTexImage (
|
273
|
+
GL_TEXTURE_RECTANGLE,
|
274
|
+
0, # mipmap level
|
275
|
+
format, #format, // GL will convert to this format
|
276
|
+
type_ #type, // Using this data type per-pixel
|
277
|
+
)
|
278
|
+
|
279
|
+
if format == GL_RED and type_ == GL_FLOAT:
|
280
|
+
assert width is None and height is None, "For this specific format, I will figure height/width myself."
|
281
|
+
|
282
|
+
# For some reasons, glGetTexImage returns a transposed array
|
283
|
+
# with an additional, useless dimension... I fix that but
|
284
|
+
# it seems strange to me.
|
285
|
+
s = list(npa.shape)
|
286
|
+
s[0], s[1] = s[1], s[0]
|
287
|
+
|
288
|
+
if len(s) == 3 and s[2] == 1:
|
289
|
+
a = np.squeeze(npa,axis=2)
|
290
|
+
return a.reshape(tuple(s[0:2]))
|
291
|
+
else:
|
292
|
+
return npa.reshape(tuple(s))
|
293
|
+
|
294
|
+
elif format == GL_RED_INTEGER and type_ == GL_UNSIGNED_BYTE:
|
295
|
+
assert width is not None and height is not None, "For this specific format, I can't figure height/width myself."
|
296
|
+
assert len(npa) == width*height, f"Dimensions {width}*{height} don't match size: {npa.size} elements"
|
297
|
+
return np.ndarray( (height, width) , np.uint8, npa)
|
298
|
+
|
299
|
+
elif format == GL_RGB_INTEGER and type_ in (GL_UNSIGNED_SHORT, GL_UNSIGNED_INT):
|
300
|
+
s = list(npa.shape)
|
301
|
+
s[0], s[1] = s[1], s[0]
|
302
|
+
|
303
|
+
if len(s) == 4 and s[2] == 1:
|
304
|
+
a = np.squeeze(npa,axis=2)
|
305
|
+
return a.reshape((s[0], s[1], 3))
|
306
|
+
else:
|
307
|
+
return npa.reshape(tuple(s))
|
308
|
+
return np.ndarray( (height, width, 3) , np.uint16, npa)
|
309
|
+
|
310
|
+
elif format == GL_RGBA and type_ == GL_FLOAT:
|
311
|
+
assert width is None and height is None, "For this specific format, I will figure height/width myself."
|
312
|
+
s = list(npa.shape)
|
313
|
+
assert len(s) == 4 and s[2] == 1
|
314
|
+
a = np.squeeze(npa,axis=2)
|
315
|
+
return a.reshape( (s[1], s[0], s[3]) )
|
316
|
+
|
317
|
+
elif format == GL_RGBA_INTEGER and type_ == GL_UNSIGNED_INT:
|
318
|
+
#assert width is None and height is None, "For this specific format, I will figure height/width myself."
|
319
|
+
s = list(npa.shape)
|
320
|
+
assert len(s) == 4 and s[2] == 1
|
321
|
+
a = np.squeeze(npa,axis=2)
|
322
|
+
return a.reshape( (s[1], s[0], s[3]) )
|
323
|
+
|
324
|
+
else:
|
325
|
+
raise Exception(f"Unsupported format/type combination: {format} - {type_}")
|
326
|
+
|
327
|
+
|
328
|
+
def read_texture(frame_buffer_id, color_attachment_ndx, width, height, internal_format):
|
329
|
+
"""
|
330
|
+
DEPRECATED Use the version 2 (this one needs framebuffers which is painful to manage).
|
331
|
+
|
332
|
+
Read a rectangle (0,0,width, height) in a texture of size at least
|
333
|
+
(width, height).
|
334
|
+
|
335
|
+
color_attachment_ndx: either an int or a GL_COLOR_ATTACHMENTx
|
336
|
+
|
337
|
+
FIXME Check things up with : https://registry.khronos.org/OpenGL-Refpages/gl4/html/glGetFramebufferAttachmentParameter.xhtml
|
338
|
+
|
339
|
+
FIXME This code is limited as we read from a frame buffer (and not directly
|
340
|
+
from a texture id). So one has to provide framebuffer and attachment number
|
341
|
+
(which is not quite convenient).
|
342
|
+
|
343
|
+
The internal format of a texture can be found in the dictioneary `textures_formats`
|
344
|
+
FIXME again, this is sub optimal. It'd be nicer to resolve a texture ID
|
345
|
+
to its corresponding frame buffer/attachment (but it dosen't make too much
|
346
|
+
sense since a texture may be attached to several FB and since I sometimes
|
347
|
+
hack the fb/textures directly (so I don't maintaint an FB/attach <-> tex. id correspondance)
|
348
|
+
"""
|
349
|
+
assert frame_buffer_id > 0
|
350
|
+
|
351
|
+
format, type_ = _get_format_type_from_internal_format(internal_format)
|
352
|
+
|
353
|
+
# This function exists to codify some of the knowledge I gathered
|
354
|
+
# while learning how to download a texture.
|
355
|
+
|
356
|
+
# The fact is that reading a texture from the GPU is much
|
357
|
+
# more difficult than reading a frame buffer color attachment.
|
358
|
+
|
359
|
+
# Right now we don't actually read from texture. We read from
|
360
|
+
# a color attachment. It means that we can only read it
|
361
|
+
# when it has the data we need.
|
362
|
+
|
363
|
+
# Make sure the right buffer is selected
|
364
|
+
# GL_FRAMEBUFFER binds framebuffer to both the read and draw framebuffer targets
|
365
|
+
|
366
|
+
# old_framebuffer_id = glGetIntegerv(GL_FRAMEBUFFER_BINDING)
|
367
|
+
# glBindFramebuffer(GL_READ_FRAMEBUFFER, old_framebuffer_id)
|
368
|
+
|
369
|
+
glBindFramebuffer(GL_READ_FRAMEBUFFER, frame_buffer_id)
|
370
|
+
logging.debug(f"Bound FrameBuffer {frame_buffer_id} for read_texture")
|
371
|
+
err = glGetError()
|
372
|
+
assert (
|
373
|
+
err == GL_NO_ERROR
|
374
|
+
), f"GL Error : {err} (0x{err:x}). glBindFramebuffer(GL_READ_FRAMEBUFFER, {frame_buffer_id}) failed."
|
375
|
+
|
376
|
+
# select a color buffer source for pixels
|
377
|
+
if color_attachment_ndx in GL_COLOR_ATTACHMENTS:
|
378
|
+
ca = color_attachment_ndx
|
379
|
+
else:
|
380
|
+
ca = GL_COLOR_ATTACHMENTS[color_attachment_ndx]
|
381
|
+
glReadBuffer(ca)
|
382
|
+
assert glGetError() == GL_NO_ERROR
|
383
|
+
|
384
|
+
# GL_FLOAT = np.float32
|
385
|
+
t = glReadPixels(0, 0, width, height, format, type_)
|
386
|
+
assert glGetError() == GL_NO_ERROR
|
387
|
+
|
388
|
+
if format == GL_RGB and type_ == GL_FLOAT:
|
389
|
+
return t.reshape(height, width, 3)
|
390
|
+
if format == GL_RGBA and type_ == GL_FLOAT:
|
391
|
+
return t.reshape(height, width, 4)
|
392
|
+
elif format == GL_RED and type_ == GL_FLOAT:
|
393
|
+
return t.reshape(height, width)
|
394
|
+
elif format == GL_RED_INTEGER and type_ == GL_INT:
|
395
|
+
return t.reshape(height, width)
|
396
|
+
elif format == GL_RED_INTEGER and type_ == GL_UNSIGNED_INT:
|
397
|
+
return t.reshape(height, width)
|
398
|
+
elif format == GL_RED_INTEGER and type_ == GL_UNSIGNED_BYTE:
|
399
|
+
# For some reason, PyOpenGL doesn't return a numpy array here but
|
400
|
+
# `bytes`. So I have to do some extra step to cast the texture into a
|
401
|
+
# numpy array myself.
|
402
|
+
return np.frombuffer(t, np.uint8).reshape(height, width)
|
403
|
+
else:
|
404
|
+
raise Exception(
|
405
|
+
f"Unsupported format/type combination: {internal_format} - {type_}"
|
406
|
+
)
|
407
|
+
|
408
|
+
def upload_geometry_to_vao(triangles: np.ndarray, attr_loc: int = 0, normalized: bool = True):
|
409
|
+
"""
|
410
|
+
Geometry is a n rows * [x,y,z] columns matrix representing the vertices,
|
411
|
+
each having x,y,z coordinates (NDC coordinates, that is, each in [-1,+1].
|
412
|
+
|
413
|
+
The vertices will be wired to the vertex attribute `attr_loc`
|
414
|
+
|
415
|
+
VAO only stores info about buffers (not transformation, not parameters,
|
416
|
+
etc.)
|
417
|
+
|
418
|
+
Returns a Vertex Array Object.
|
419
|
+
"""
|
420
|
+
|
421
|
+
assert triangles.dtype == np.float32
|
422
|
+
vao_id = glGenVertexArrays(1)
|
423
|
+
glBindVertexArray(vao_id)
|
424
|
+
|
425
|
+
vertex_buffer_object_id = glGenBuffers(1)
|
426
|
+
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object_id)
|
427
|
+
# OpenGL doc: creates and initializes a buffer object's data store
|
428
|
+
glBufferData(GL_ARRAY_BUFFER, triangles.nbytes, triangles.ravel(), GL_STATIC_DRAW)
|
429
|
+
|
430
|
+
# print(f"Size: {triangles.nbytes} bytes")
|
431
|
+
|
432
|
+
# NOTE You don't need to glUseProgram() before; this is just an
|
433
|
+
# attribute number.
|
434
|
+
# Array access is enabled by binding the VAO in question and calling
|
435
|
+
glEnableVertexAttribArray(attr_loc)
|
436
|
+
|
437
|
+
# Tell OpenGL we want the attribute 0 to be enabled and wired to our vertices coordinates.
|
438
|
+
# In the vertex shader, we'll have to do something like:
|
439
|
+
# layout(location = 0) in vec4 my_vertex;
|
440
|
+
# to connect that "attribute 0" to the shader.
|
441
|
+
|
442
|
+
if normalized:
|
443
|
+
gl_norm = GL_TRUE
|
444
|
+
else:
|
445
|
+
gl_norm = GL_FALSE
|
446
|
+
|
447
|
+
glVertexAttribPointer(attr_loc, 3, GL_FLOAT, gl_norm, 0, None)
|
448
|
+
|
449
|
+
# order important else you clear the buffer's bind located in
|
450
|
+
# the vertex array object :-)
|
451
|
+
glBindVertexArray(GL_NONE)
|
452
|
+
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE)
|
453
|
+
return vao_id
|
454
|
+
|
455
|
+
|
456
|
+
def make_quad(xmin, xmax, ymin, ymax):
|
457
|
+
# print(f"{xmin:.2f}-{xmax:.2f}: {ymin:.2f}-{ymax:.2f}")
|
458
|
+
quad_vertices = [
|
459
|
+
(xmax, ymin, 0.5),
|
460
|
+
(xmax, ymax, 0.5),
|
461
|
+
(xmin, ymax, 0.5),
|
462
|
+
(xmin, ymin, 0.5),
|
463
|
+
]
|
464
|
+
|
465
|
+
quad_vertex_triangles = [(0, 1, 2), (0, 2, 3)]
|
466
|
+
|
467
|
+
quad_normals = [(0.000000, 0.000000, 1.000000), (0.000000, 0.000000, 1.000000)]
|
468
|
+
|
469
|
+
quad_texcoords = [(1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)]
|
470
|
+
|
471
|
+
quad_texture_triangles = [(0, 1, 2), (0, 2, 3)]
|
472
|
+
|
473
|
+
quad_normal_triangles = [(1, 1, 1), (1, 1, 1)]
|
474
|
+
|
475
|
+
np_quad_vertices = np.array(
|
476
|
+
[quad_vertices[index] for indices in quad_vertex_triangles for index in indices]
|
477
|
+
)
|
478
|
+
|
479
|
+
np_quad_normals = np.array(
|
480
|
+
[quad_normals[index] for indices in quad_normal_triangles for index in indices]
|
481
|
+
)
|
482
|
+
|
483
|
+
np_quad_texcoords = np.array(
|
484
|
+
[
|
485
|
+
quad_texcoords[index]
|
486
|
+
for indices in quad_texture_triangles
|
487
|
+
for index in indices
|
488
|
+
]
|
489
|
+
)
|
490
|
+
|
491
|
+
# Texture coordintaes remain in floats
|
492
|
+
np_quad_texcoords_rectangle = np_quad_texcoords
|
493
|
+
return np_quad_vertices, np_quad_texcoords_rectangle
|
494
|
+
|
495
|
+
|
496
|
+
def make_unit_quad(texture_width, texture_height):
|
497
|
+
return make_quad(-1.0, 1.0, -1.0, 1.0)
|
498
|
+
|
499
|
+
|
500
|
+
def query_gl_caps():
|
501
|
+
""" Query the GPU for its capabilities. """
|
502
|
+
MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB = (
|
503
|
+
35881 # useful for geometry shader limitations; see https://community.khronos.org/t/textures-in-the-geometry-shader/75766/3
|
504
|
+
)
|
505
|
+
|
506
|
+
from OpenGL.GL import (
|
507
|
+
GL_MAX_COMPUTE_WORK_GROUP_SIZE,
|
508
|
+
glGetIntegeri_v,
|
509
|
+
glGetInteger,
|
510
|
+
GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS,
|
511
|
+
GL_MAX_COMPUTE_WORK_GROUP_COUNT,
|
512
|
+
GL_MAX_COLOR_ATTACHMENTS,
|
513
|
+
GL_MAX_DRAW_BUFFERS,
|
514
|
+
GL_MAX_TEXTURE_IMAGE_UNITS,
|
515
|
+
GL_MAX_VERTEX_UNIFORM_VECTORS,
|
516
|
+
GL_MAX_FRAGMENT_UNIFORM_VECTORS,
|
517
|
+
GL_MAX_TEXTURE_SIZE,
|
518
|
+
GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS,
|
519
|
+
GL_MAX_SHADER_STORAGE_BLOCK_SIZE,
|
520
|
+
GL_MAX_TEXTURE_BUFFER_SIZE,
|
521
|
+
)
|
522
|
+
from math import sqrt
|
523
|
+
|
524
|
+
logging.info(
|
525
|
+
f"OpenGl version: {glGetIntegerv(GL_MAJOR_VERSION)}.{glGetIntegerv(GL_MINOR_VERSION)}; {glGetString(GL_VERSION)}; {glGetString(GL_VENDOR)} -- GL/SL:{glGetString(GL_SHADING_LANGUAGE_VERSION) }; MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB={glGetInteger(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB)} max viewport={glGetIntegerv(GL_MAX_VIEWPORT_DIMS)}"
|
526
|
+
)
|
527
|
+
# Maximum dimension of a texture => maximum size of the computation domain.
|
528
|
+
# In cas you wonder, it's pretty difficult (and totally out of the OpenGL spec) to
|
529
|
+
# know how much memory there is on the GPU. That's because memory is swapped there
|
530
|
+
# too, just like in any OS. And therefore it's also tricky (impossible ?) to know which texture
|
531
|
+
# is in GPU RAM or swapped out...
|
532
|
+
logging.info("Texture info:")
|
533
|
+
logging.info(
|
534
|
+
f"GL_MAX_TEXTURE_SIZE = {glGetIntegerv(GL_MAX_TEXTURE_SIZE)}x{glGetIntegerv(GL_MAX_TEXTURE_SIZE)} texels"
|
535
|
+
)
|
536
|
+
max_buffer_size = glGetInteger(
|
537
|
+
GL_MAX_TEXTURE_BUFFER_SIZE
|
538
|
+
) # In texels ! See: https://www.khronos.org/opengl/wiki/Buffer_Texture
|
539
|
+
logging.info(
|
540
|
+
f"GL_MAX_TEXTURE_BUFFER_SIZE = {max_buffer_size / (1024**2):.1f} mega-texels"
|
541
|
+
)
|
542
|
+
d = int(sqrt(max_buffer_size))
|
543
|
+
logging.info(f"Texture buffer max square size given memory limit.: {d}x{d} texels")
|
544
|
+
|
545
|
+
logging.info("SSBO info:")
|
546
|
+
max_ssbo_size = glGetInteger(GL_MAX_SHADER_STORAGE_BLOCK_SIZE)
|
547
|
+
logging.info(
|
548
|
+
f"GL_MAX_SHADER_STORAGE_BLOCK_SIZE = {max_ssbo_size / (1024**2):.1f} Mbytes"
|
549
|
+
)
|
550
|
+
d = int(sqrt(max_ssbo_size / 16))
|
551
|
+
logging.info(f"SSBO max square size: {d}x{d} RGBAf32 elements")
|
552
|
+
# logging.info(f"If one float per buffer SSBO max square size: {d}x{d} RGBAf32 elements")
|
553
|
+
|
554
|
+
logging.info(
|
555
|
+
f"GL_MAX_COMPUTE_WORK_GROUP_COUNT = x:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT,0)[0]} y:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT,0)[0]}"
|
556
|
+
)
|
557
|
+
logging.info(
|
558
|
+
f"GL_MAX_COMPUTE_WORK_GROUP_SIZE = x:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE,0)[0]} y:{glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE,1)[0]} (== max tile size)"
|
559
|
+
)
|
560
|
+
logging.info(
|
561
|
+
f"GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS = { glGetInteger(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS)}"
|
562
|
+
)
|
563
|
+
|
564
|
+
logging.info(
|
565
|
+
f"GL_MAX_COLOR_ATTACHMENTS = { glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS)}"
|
566
|
+
)
|
567
|
+
logging.info(f"GL_MAX_DRAW_BUFFERS = { glGetIntegerv(GL_MAX_DRAW_BUFFERS)}")
|
568
|
+
logging.info(
|
569
|
+
f"GL_MAX_TEXTURE_IMAGE_UNITS = { glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS)}"
|
570
|
+
)
|
571
|
+
logging.info(
|
572
|
+
f"GL_MAX_VERTEX_UNIFORM_VECTORS = { glGetIntegerv(GL_MAX_VERTEX_UNIFORM_VECTORS)}"
|
573
|
+
)
|
574
|
+
logging.info(
|
575
|
+
f"GL_MAX_FRAGMENT_UNIFORM_VECTORS = { glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_VECTORS)}"
|
576
|
+
)
|
577
|
+
|
578
|
+
logging.info(
|
579
|
+
f"GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS = { glGetIntegerv(GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS)}"
|
580
|
+
)
|
581
|
+
|
582
|
+
# I don't think we use these
|
583
|
+
# logging.debug(f"GL_MAX_ARRAY_TEXTURE_LAYERS = { glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS)}")
|
584
|
+
|
585
|
+
|
586
|
+
|
587
|
+
class GL_cache_tools():
|
588
|
+
""" This class helps to upload shaders and textures in the GPU. """
|
589
|
+
|
590
|
+
samplers_in_shader: dict[tuple[int, str], str]
|
591
|
+
shaders_programs: dict[int, int] = dict() # program id -> shaders
|
592
|
+
shaders_names: dict[int, str] = dict() # shader_id -> string
|
593
|
+
|
594
|
+
def __init__(self) -> None:
|
595
|
+
|
596
|
+
# Recall the type of a sampler associated to a shader.
|
597
|
+
# So maps (shader id, sampler's name) to sampler's type name.
|
598
|
+
self.samplers_in_shader = dict()
|
599
|
+
|
600
|
+
self.all_textures_sizes = dict()
|
601
|
+
|
602
|
+
# Maps texture id to (context, texture internal format)
|
603
|
+
# Texture interanl format are loike: GL_RGB32F...
|
604
|
+
self.textures_formats = dict()
|
605
|
+
|
606
|
+
self._set_uniform_cache = dict()
|
607
|
+
|
608
|
+
# def gl_clear_all_caches(self):
|
609
|
+
# self.samplers_in_shader.clear()
|
610
|
+
# self.shaders_programs.clear()
|
611
|
+
# self.shaders_names.clear()
|
612
|
+
# self.all_textures_sizes.clear()
|
613
|
+
# self.textures_formats.clear()
|
614
|
+
# self._set_uniform_cache.clear()
|
615
|
+
|
616
|
+
# def clear_uniform_cache(self):
|
617
|
+
# self._set_uniform_cache.clear()
|
618
|
+
|
619
|
+
def describe_program(self, pid:int):
|
620
|
+
return ",".join([self.shaders_names[sid] for sid in self.shaders_programs[pid]])
|
621
|
+
|
622
|
+
|
623
|
+
def load_shader_from_source(self, shader_type, source: str) -> int:
|
624
|
+
assert shader_type in (
|
625
|
+
GL_VERTEX_SHADER,
|
626
|
+
GL_FRAGMENT_SHADER,
|
627
|
+
GL_GEOMETRY_SHADER,
|
628
|
+
GL_COMPUTE_SHADER,
|
629
|
+
)
|
630
|
+
|
631
|
+
# OpenGL will silently do nothing if you don't activate the extension
|
632
|
+
# which is pretty tough to debug.
|
633
|
+
# https://www.khronos.org/opengl/wiki/GL_EXT_texture_integer
|
634
|
+
|
635
|
+
gl_ext_texture_integer = (
|
636
|
+
"GL_EXT_gpu_shader4" in source
|
637
|
+
or "#extension GL_ARB_texture_rectangle" in source
|
638
|
+
)
|
639
|
+
for s in ["usampler2DRect", "isampler2DRect", "sampler2DRect"]:
|
640
|
+
if s in source:
|
641
|
+
assert (
|
642
|
+
gl_ext_texture_integer
|
643
|
+
), f"To use {s} you need the extension GL_EXT_gpu_shader4"
|
644
|
+
|
645
|
+
shader_id: int = glCreateShader(shader_type) # type: ignore
|
646
|
+
|
647
|
+
if shader_id == 0:
|
648
|
+
raise Exception("Shader loading failed")
|
649
|
+
return 0
|
650
|
+
|
651
|
+
glShaderSource(shader_id, source)
|
652
|
+
glCompileShader(shader_id)
|
653
|
+
|
654
|
+
if glGetShaderiv(shader_id, GL_COMPILE_STATUS, None) == GL_FALSE:
|
655
|
+
info_log = glGetShaderInfoLog(shader_id)
|
656
|
+
logging.error(info_log.decode("ASCII"))
|
657
|
+
try:
|
658
|
+
glDeleteProgram(shader_id)
|
659
|
+
except:
|
660
|
+
pass
|
661
|
+
finally:
|
662
|
+
raise Exception(f"Unable to load shader. {info_log.decode('ASCII')}")
|
663
|
+
|
664
|
+
# Keep track of types.
|
665
|
+
for line in source.split("\n"):
|
666
|
+
m = TEX_SAMPLERS_RE.match(line)
|
667
|
+
if m:
|
668
|
+
type_name = m.groups()[0]
|
669
|
+
sampler_name = m.groups()[1]
|
670
|
+
logging.debug(
|
671
|
+
f"Load shader: (shader:{shader_id}, sampler name:{sampler_name}) -> type={type_name}"
|
672
|
+
)
|
673
|
+
self.samplers_in_shader[(shader_id, sampler_name)] = type_name
|
674
|
+
|
675
|
+
self.shaders_names[shader_id] = "from source"
|
676
|
+
return shader_id
|
677
|
+
|
678
|
+
|
679
|
+
|
680
|
+
def track_texture_size(self, tex_id:int, img_data, w:int, h:int, format) -> int:
|
681
|
+
if isinstance(img_data, np.ndarray):
|
682
|
+
s = img_data.nbytes
|
683
|
+
else:
|
684
|
+
s = ctypes.sizeof(img_data)
|
685
|
+
|
686
|
+
logging.debug(
|
687
|
+
f"Uploaded {w}x{h} texels, {s} bytes ({s/(1024**2):.1f} MB) to GPU, format={format}"
|
688
|
+
)
|
689
|
+
self.all_textures_sizes[tex_id] = s
|
690
|
+
return s
|
691
|
+
|
692
|
+
def total_textures_size(self) -> int:
|
693
|
+
""" Return the total size of all textures in bytes. """
|
694
|
+
s = []
|
695
|
+
for tex_id, size in self.all_textures_sizes.items():
|
696
|
+
s.append( size )
|
697
|
+
return sum(s)
|
698
|
+
|
699
|
+
|
700
|
+
def drop_textures(self, texture_ids: Union[list[int], int]):
|
701
|
+
""" Drop one or more textures. Expect texture id's.
|
702
|
+
"""
|
703
|
+
assert texture_ids is not None
|
704
|
+
if not isinstance(texture_ids, list):
|
705
|
+
texture_ids = [texture_ids]
|
706
|
+
|
707
|
+
# In some rare occurences, we reuse twice the same
|
708
|
+
# texture.
|
709
|
+
texture_ids = list(set(texture_ids))
|
710
|
+
for tid in texture_ids:
|
711
|
+
assert tid in self.textures_formats, f"Never seen that texture `{tid}` before"
|
712
|
+
del self.textures_formats[tid]
|
713
|
+
|
714
|
+
assert glGetError() == GL_NONE
|
715
|
+
#print(f"deleteing {texture_ids}")
|
716
|
+
glDeleteTextures(texture_ids)
|
717
|
+
|
718
|
+
|
719
|
+
def upload_np_array_to_gpu(self,
|
720
|
+
context,
|
721
|
+
format,
|
722
|
+
img_data: np.ndarray,
|
723
|
+
texture_id: Union[int, None] = None
|
724
|
+
) -> int:
|
725
|
+
"""The goal of this function is to standardize textures
|
726
|
+
configuration and upload to GPU. We trade generality for
|
727
|
+
ease of use.
|
728
|
+
|
729
|
+
If you pass in a texture_id, then the texture will be updated
|
730
|
+
instead of created.
|
731
|
+
|
732
|
+
Returns the texture OpenGL id.
|
733
|
+
"""
|
734
|
+
|
735
|
+
assert context in (GL_TEXTURE_RECTANGLE, GL_TEXTURE_2D)
|
736
|
+
assert isinstance(img_data, np.ndarray)
|
737
|
+
|
738
|
+
# From : https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
|
739
|
+
|
740
|
+
# "The first element corresponds to the lower left corner of the texture
|
741
|
+
# image. Subsequent elements progress left-to-right through the remaining
|
742
|
+
# texels in the lowest row of the texture image, and then in successively
|
743
|
+
# higher rows of the texture image. The final element corresponds to the
|
744
|
+
# upper right corner of the texture image. "
|
745
|
+
|
746
|
+
assert img_data.flags["C"], "I believe pyOpenGL prefer C-contiguous data array"
|
747
|
+
|
748
|
+
if texture_id is None:
|
749
|
+
new_texture = True
|
750
|
+
texture_id = glGenTextures(1) # Name one new texture
|
751
|
+
# if texture_id == 10:
|
752
|
+
# print("**********************************************")
|
753
|
+
# print_stack()
|
754
|
+
#assert texture_id not in textures_formats, f"I just generated a texture ID that already exists in my database ({textures_formats[texture_id]}) ??? Maybe you need to clear the DB first ?"
|
755
|
+
self.textures_formats[texture_id] = (context, format)
|
756
|
+
else:
|
757
|
+
assert (
|
758
|
+
texture_id in self.textures_formats
|
759
|
+
), "Can't update a texture I have never seen before"
|
760
|
+
assert self.textures_formats[texture_id] == (
|
761
|
+
context,
|
762
|
+
format,
|
763
|
+
), "You're changing the nature of the texture"
|
764
|
+
new_texture = False
|
765
|
+
|
766
|
+
glBindTexture(
|
767
|
+
context, texture_id
|
768
|
+
) # Bind the texture to context target (kind of assocaiting it to a type)
|
769
|
+
|
770
|
+
# Prevent texture interpolation with samplers
|
771
|
+
glTexParameteri(context, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
|
772
|
+
glTexParameteri(context, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
|
773
|
+
# GL_CLAMP_TO_EDGE: Clamps the coordinates between 0 and 1. The result is
|
774
|
+
# that higher coordinates become clamped to the edge, resulting in a
|
775
|
+
# stretched edge pattern.
|
776
|
+
glTexParameteri(context, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
777
|
+
glTexParameteri(context, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
778
|
+
|
779
|
+
if format == GL_R32F:
|
780
|
+
assert len(img_data.shape) == 2, "We only accept 2D textures for GL_R32F format"
|
781
|
+
assert img_data.dtype in (
|
782
|
+
float,
|
783
|
+
np.float32,
|
784
|
+
), f"We only accept floats, you gave {img_data.dtype}"
|
785
|
+
h, w = img_data.shape
|
786
|
+
# internal format; format; type.
|
787
|
+
glTexImage2D(context, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, img_data)
|
788
|
+
elif format == GL_RGB32F:
|
789
|
+
assert (
|
790
|
+
len(img_data.shape) == 3 and img_data.shape[2] == 3
|
791
|
+
), "We only accept 2D RGB textures, shape=(h,w,3) for GL_RGB32F format"
|
792
|
+
assert img_data.dtype in (float, np.float32)
|
793
|
+
h, w, _ = img_data.shape
|
794
|
+
glTexImage2D(context, 0, GL_RGB32F, w, h, 0, GL_RGB, GL_FLOAT, img_data)
|
795
|
+
elif format == GL_RG16UI:
|
796
|
+
assert (
|
797
|
+
len(img_data.shape) == 3 and img_data.shape[2] == 2
|
798
|
+
), "We only accept 2D RG textures, shape=(h,w,2) for GL_RG16UI format"
|
799
|
+
assert img_data.dtype == np.uint16
|
800
|
+
h, w, _ = img_data.shape
|
801
|
+
# FIXME Why do I need to suffix GL_RG with INTEGER (I don't do it elsewhere
|
802
|
+
# but here it is mandatory)
|
803
|
+
glTexImage2D(context, 0, GL_RG16UI, w, h, 0, GL_RG_INTEGER, GL_UNSIGNED_SHORT, img_data)
|
804
|
+
elif format == GL_RGB16UI:
|
805
|
+
assert (
|
806
|
+
len(img_data.shape) == 3 and img_data.shape[2] == 3
|
807
|
+
), f"We only accept 2D RGB textures, shape=(h,w,3) for GL_RGB16UI format. You gave: {img_data.shape}"
|
808
|
+
assert img_data.dtype == np.uint16, "Expecting unsigned short"
|
809
|
+
h, w, _ = img_data.shape
|
810
|
+
glTexImage2D(context, 0, GL_RGB16UI, w, h, 0, GL_RGB_INTEGER, GL_UNSIGNED_SHORT, img_data)
|
811
|
+
elif format == GL_RGB32UI:
|
812
|
+
assert (
|
813
|
+
len(img_data.shape) == 3 and img_data.shape[2] == 3
|
814
|
+
), f"We only accept 2D RGB textures, shape=(h,w,3) for GL_RGB16UI format. You gave: {img_data.shape}"
|
815
|
+
assert img_data.dtype == np.uint32, f"Expecting unsigned int, got {img_data.dtype}"
|
816
|
+
h, w, _ = img_data.shape
|
817
|
+
glTexImage2D(context, 0, GL_RGB32UI, w, h, 0, GL_RGB_INTEGER, GL_UNSIGNED_INT, img_data)
|
818
|
+
elif format == GL_RGBA32F:
|
819
|
+
assert (
|
820
|
+
len(img_data.shape) == 3 and img_data.shape[2] == 4
|
821
|
+
), "We only accept 2D RGBA textures, shape=(h,w,3) for GL_RGBA32F format"
|
822
|
+
assert img_data.dtype in (float, np.float32)
|
823
|
+
h, w, _ = img_data.shape
|
824
|
+
glTexImage2D(context, 0, GL_RGBA32F, w, h, 0, GL_RGBA, GL_FLOAT, img_data)
|
825
|
+
elif format == GL_RGBA32UI:
|
826
|
+
assert (
|
827
|
+
len(img_data.shape) == 3 and img_data.shape[2] == 4
|
828
|
+
), f"We only accept 2D RGBA textures, shape=(h,w,3) for GL_RGBA32UI format, you gave {img_data.shape}"
|
829
|
+
assert img_data.dtype in (np.uint32, )
|
830
|
+
h, w, _ = img_data.shape
|
831
|
+
glTexImage2D(context, 0, GL_RGBA32UI, w, h, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, img_data)
|
832
|
+
elif format == GL_R32UI:
|
833
|
+
assert (
|
834
|
+
len(img_data.shape) == 2
|
835
|
+
), "We only accept 2D textures for GL_R32UI format"
|
836
|
+
assert (
|
837
|
+
img_data.dtype == np.uint32
|
838
|
+
), f"We only accept uint32, you gave {img_data.dtype}"
|
839
|
+
h, w = img_data.shape
|
840
|
+
# See https://stackoverflow.com/questions/59542891/opengl-integer-texture-raising-gl-invalid-value
|
841
|
+
# GL_RED_INTEGER is for unnormalized values (such as uint's)
|
842
|
+
glTexImage2D(
|
843
|
+
context, 0, GL_R32UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, img_data
|
844
|
+
)
|
845
|
+
elif format == GL_R8UI:
|
846
|
+
assert (
|
847
|
+
len(img_data.shape) == 2
|
848
|
+
), "We only accept 2D textures for GL_R32UI format"
|
849
|
+
assert (
|
850
|
+
img_data.dtype == np.uint8
|
851
|
+
), f"We only accept uint8, you gave {img_data.dtype}"
|
852
|
+
h, w = img_data.shape
|
853
|
+
glTexImage2D(
|
854
|
+
context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, img_data
|
855
|
+
)
|
856
|
+
elif format == GL_R32I:
|
857
|
+
assert len(img_data.shape) == 2, "We only accept 2D textures for GL_R32I format"
|
858
|
+
assert (
|
859
|
+
img_data.dtype == np.int32
|
860
|
+
), f"We only accept int32, you gave {img_data.dtype}"
|
861
|
+
h, w = img_data.shape
|
862
|
+
glTexImage2D(context, 0, GL_R32I, w, h, 0, GL_RED_INTEGER, GL_INT, img_data)
|
863
|
+
else:
|
864
|
+
raise Exception(f"Unsupported format {format}")
|
865
|
+
|
866
|
+
check_gl_error()
|
867
|
+
self.track_texture_size(texture_id, img_data, w, h, format)
|
868
|
+
return texture_id
|
869
|
+
|
870
|
+
|
871
|
+
def upload_blank_texture_to_gpu(self, w: int, h: int, context, format=GL_R32F, value=0.0):
|
872
|
+
"""
|
873
|
+
Make and upload a blank (or one color) texture to the GPU.
|
874
|
+
|
875
|
+
`format`: some texture format. The way it is done here means that
|
876
|
+
we will derive the texture format in the GPU as well as the texture
|
877
|
+
format of the "value" data.
|
878
|
+
`context`: either GL_TEXTURE_RECTANGLE, GL_TEXTURE_2D
|
879
|
+
`value`: will be set on each components of the texels.
|
880
|
+
"""
|
881
|
+
# FIXME Wire this to upload_np_array_to_gpu(...)
|
882
|
+
|
883
|
+
assert context in (GL_TEXTURE_RECTANGLE, GL_TEXTURE_2D)
|
884
|
+
|
885
|
+
texture_id = glGenTextures(1) # Name one new texture
|
886
|
+
# if texture_id == 10:
|
887
|
+
# print("**********************************************")
|
888
|
+
# print_stack()
|
889
|
+
#assert texture_id not in textures_formats, "A brand new texture can't already exist!"
|
890
|
+
self.textures_formats[texture_id] = (context, format)
|
891
|
+
glBindTexture(
|
892
|
+
context, texture_id
|
893
|
+
) # Bind the texture to context target (kind of assocaiting it to a type)
|
894
|
+
glTexParameteri(context, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
|
895
|
+
glTexParameteri(context, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
|
896
|
+
glTexParameteri(context, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
897
|
+
glTexParameteri(context, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
898
|
+
|
899
|
+
#logging.debug(f"Uploading {w}x{h} constant pixels to GPU. Format= {format}")
|
900
|
+
|
901
|
+
if format in (GL_R32UI, GL_R8UI, GL_RGBA8) and value == 0.0:
|
902
|
+
# Quality of life when leaving value to its default.
|
903
|
+
value = 0
|
904
|
+
|
905
|
+
if format == GL_R32F:
|
906
|
+
img_data = (ctypes.c_float * (w * h * 1))(*([value] * (w * h * 1)))
|
907
|
+
glTexImage2D(context, 0, GL_R32F, w, h, 0, GL_RED, GL_FLOAT, img_data)
|
908
|
+
elif format == GL_RG32F:
|
909
|
+
img_data = (ctypes.c_float * (w * h * 2))(*([value] * (w * h * 2)))
|
910
|
+
glTexImage2D(context, 0, GL_RG32F, w, h, 0, GL_RG, GL_FLOAT, img_data)
|
911
|
+
elif format == GL_RGB32F:
|
912
|
+
if type(value) == list:
|
913
|
+
value: list
|
914
|
+
assert len(value) == 3, "I expect three components for R,G,B"
|
915
|
+
img_data = (ctypes.c_float * (w * h * 3))(*(value * (w * h)))
|
916
|
+
else:
|
917
|
+
# img_data = (ctypes.c_float * (w * h * 3))(*([value] * (w*h*3)))
|
918
|
+
img_data = np.full((w * h * 3,), value, dtype=np.float32)
|
919
|
+
glTexImage2D(context, 0, GL_RGB32F, w, h, 0, GL_RGB, GL_FLOAT, img_data)
|
920
|
+
elif format == GL_RGBA32F:
|
921
|
+
if type(value) == list:
|
922
|
+
value: list
|
923
|
+
assert len(value) == 4, "I expect three components for R,G,B"
|
924
|
+
img_data = (ctypes.c_float * (w * h * 4))(*(value * (w * h)))
|
925
|
+
else:
|
926
|
+
# img_data = (ctypes.c_float * (w * h * 4))(*([value] * (w*h*4)))
|
927
|
+
img_data = np.full((w * h * 4,), value, dtype=np.float32)
|
928
|
+
|
929
|
+
glTexImage2D(context, 0, GL_RGBA32F, w, h, 0, GL_RGBA, GL_FLOAT, img_data)
|
930
|
+
elif format == GL_RGBA8:
|
931
|
+
if type(value) == list:
|
932
|
+
value: list
|
933
|
+
assert len(value) == 4, "I expect three components for R,G,B"
|
934
|
+
img_data = (ctypes.c_uint8 * (w * h * 4))(*(value * (w * h)))
|
935
|
+
else:
|
936
|
+
assert type(value) in (
|
937
|
+
np.uint8,
|
938
|
+
int,
|
939
|
+
), f"I expect unsigned 8 bits integer, you gave {type(value)}"
|
940
|
+
assert 0 <= value <= 255
|
941
|
+
img_data = (ctypes.c_uint8 * (w * h * 4))(*([value] * (w * h * 4)))
|
942
|
+
# glTexImage2D( ctx, "base internal formats" (see Table2 and Table1 of https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml )==format of the texture in the GPU memory,
|
943
|
+
# w,h,0, format=format of the source pixels data, typz=data type of the source pixels data)
|
944
|
+
glTexImage2D(context, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data)
|
945
|
+
elif format == GL_R32UI:
|
946
|
+
assert type(value) == int and value >= 0
|
947
|
+
img_data = (ctypes.c_uint32 * (w * h * 1))(*([value] * (w * h * 1)))
|
948
|
+
# internal format; format (of pixel data); (data) type (of pixel data)
|
949
|
+
glTexImage2D(
|
950
|
+
context, 0, GL_R32UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_INT, img_data
|
951
|
+
)
|
952
|
+
elif format == GL_R8UI:
|
953
|
+
logging.debug("Creataing data")
|
954
|
+
assert type(value) in (
|
955
|
+
np.uint8,
|
956
|
+
int,
|
957
|
+
), f"I expect unsigned 8 bits integer, you gave {type(value)}"
|
958
|
+
assert 0 <= value <= 255
|
959
|
+
|
960
|
+
# I tried to allocate memory in an 4-bytes aligned way
|
961
|
+
# but it doesn't work => I use glPixelStore
|
962
|
+
img_data = np.full((h * w,), value, dtype=np.uint8)
|
963
|
+
logging.debug("Uploading data")
|
964
|
+
pixels_align_old = glGetInteger(GL_UNPACK_ALIGNMENT)
|
965
|
+
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
|
966
|
+
glTexImage2D(
|
967
|
+
context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, img_data
|
968
|
+
)
|
969
|
+
glPixelStorei(GL_UNPACK_ALIGNMENT, pixels_align_old) # Restore old value
|
970
|
+
logging.debug("Uploaded data")
|
971
|
+
|
972
|
+
# size = w*h
|
973
|
+
# logging.debug(f"memory_aligned_byte_array size={size} value={value}")
|
974
|
+
# assert ctypes.alignment(ctypes.c_uint32) == 4
|
975
|
+
# assert ctypes.alignment(ctypes.c_uint32) == glGetInteger(GL_UNPACK_ALIGNMENT), "We must align on OpenGL alignment"
|
976
|
+
# v = value << 24 + value << 16 + value << 8 + value
|
977
|
+
# padded_len = (size + 3) // 4
|
978
|
+
# img_data = (ctypes.c_uint32 * padded_len)(*([v] * padded_len))
|
979
|
+
# assert ctypes.addressof(img_data) % 4 == 0
|
980
|
+
# img_data2 = (ctypes.c_uint8 * size).from_buffer(img_data)
|
981
|
+
# logging.debug("memory_aligned_byte_array - done")
|
982
|
+
# img_data2 = np.frombuffer(img_data2, dtype=np.uint8)
|
983
|
+
# glTexImage2D(context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, img_data2)
|
984
|
+
|
985
|
+
elif format == GL_R32I:
|
986
|
+
assert type(value) == int
|
987
|
+
# img_data = (ctypes.c_int32 * (w * h * 1))(*([value] * (w*h*1)))
|
988
|
+
img_data = np.full((h * w,), value, dtype=np.int32)
|
989
|
+
# internal format; format (of pixel data); (data) type (of pixel data)
|
990
|
+
# glTexImage2D(context, 0, GL_R8UI, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, img_data)
|
991
|
+
glTexImage2D(context, 0, GL_R32I, w, h, 0, GL_RED_INTEGER, GL_INT, img_data)
|
992
|
+
else:
|
993
|
+
raise Exception(f"Unsupported texture format : {format}")
|
994
|
+
|
995
|
+
check_gl_error()
|
996
|
+
self.track_texture_size(texture_id, img_data, w, h, format)
|
997
|
+
|
998
|
+
return texture_id
|
999
|
+
|
1000
|
+
|
1001
|
+
def clear_texture(self, texture_id:int):
|
1002
|
+
assert (
|
1003
|
+
texture_id in self.textures_formats
|
1004
|
+
), "I can't find your texture_id (textures_formats) in the textures I have created..."
|
1005
|
+
context_, internal_format = self.textures_formats[texture_id]
|
1006
|
+
|
1007
|
+
if internal_format == GL_R32F:
|
1008
|
+
format, type_ = GL_RED, GL_FLOAT
|
1009
|
+
elif internal_format == GL_RGB32F:
|
1010
|
+
format, type_ = GL_RGB, GL_FLOAT
|
1011
|
+
elif internal_format == GL_RGBA32F:
|
1012
|
+
format, type_ = GL_RGBA, GL_FLOAT
|
1013
|
+
elif internal_format == GL_R32UI:
|
1014
|
+
format, type_ = GL_RED_INTEGER, GL_UNSIGNED_INT
|
1015
|
+
# See https://stackoverflow.com/questions/59542891/opengl-integer-texture-raising-gl-invalid-value
|
1016
|
+
# GL_RED_INTEGER is for unnormalized values (such as uint's)
|
1017
|
+
elif internal_format == GL_R8UI:
|
1018
|
+
format, type_ = GL_RED_INTEGER, GL_UNSIGNED_BYTE
|
1019
|
+
elif internal_format == GL_R32I:
|
1020
|
+
format, type_ = GL_RED_INTEGER, GL_INT
|
1021
|
+
else:
|
1022
|
+
raise Exception(f"Unsupported format {internal_format}")
|
1023
|
+
|
1024
|
+
if type_ == GL_FLOAT:
|
1025
|
+
ct = ctypes.c_float
|
1026
|
+
elif type_ == GL_INT:
|
1027
|
+
ct = ctypes.c_int32
|
1028
|
+
elif type_ == GL_UNSIGNED_BYTE:
|
1029
|
+
ct = ctypes.c_uint8
|
1030
|
+
else:
|
1031
|
+
raise Exception("Unsupported type : {}", type_)
|
1032
|
+
|
1033
|
+
if format in (GL_RED, GL_RED_INTEGER):
|
1034
|
+
size = 1
|
1035
|
+
elif format == GL_RGB:
|
1036
|
+
size = 3
|
1037
|
+
elif format == GL_RGBA:
|
1038
|
+
size = 4
|
1039
|
+
else:
|
1040
|
+
raise Exception("Unsupported format : {}", format)
|
1041
|
+
|
1042
|
+
img_data = (ct * size)(*([0] * size))
|
1043
|
+
glClearTexImage(texture_id, 0, format, type_, img_data)
|
1044
|
+
|
1045
|
+
|
1046
|
+
|
1047
|
+
def load_shader_from_file(self, fpath: Path, log_path: Path = None, shader_type=None):
|
1048
|
+
""" Load a shader from a file - can contain %%includes.
|
1049
|
+
The extension of the file determines the type of shader :
|
1050
|
+
- .vs : vertex shader
|
1051
|
+
- .frs : fragment shader
|
1052
|
+
- .gs : geometry shader
|
1053
|
+
- .comp : compute shader (.cs is deprecated)
|
1054
|
+
"""
|
1055
|
+
shader_dir = fpath.parent
|
1056
|
+
with open(fpath, "r") as source:
|
1057
|
+
|
1058
|
+
if shader_type is None:
|
1059
|
+
suffix = fpath.suffix
|
1060
|
+
if suffix == ".vs":
|
1061
|
+
shader_type = GL_VERTEX_SHADER
|
1062
|
+
elif suffix == ".frs":
|
1063
|
+
shader_type = GL_FRAGMENT_SHADER
|
1064
|
+
elif suffix == ".gs":
|
1065
|
+
shader_type = GL_GEOMETRY_SHADER
|
1066
|
+
elif suffix in (".comp", ".cs"):
|
1067
|
+
if suffix == ".cs":
|
1068
|
+
logging.warning(".cs extension is deprecated")
|
1069
|
+
|
1070
|
+
shader_type = GL_COMPUTE_SHADER
|
1071
|
+
else:
|
1072
|
+
raise Exception(f"Unrecognized shader extension {suffix}")
|
1073
|
+
else:
|
1074
|
+
assert shader_type in [GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER, GL_COMPUTE_SHADER], f"Unknown shader type {shader_type}"
|
1075
|
+
|
1076
|
+
try:
|
1077
|
+
# print(f"Loading {fpath} as {shader_type}")
|
1078
|
+
INCLUDE_MARK = "%%include"
|
1079
|
+
source_lines = []
|
1080
|
+
for line in source.readlines():
|
1081
|
+
if line.strip().startswith(INCLUDE_MARK):
|
1082
|
+
fn = shader_dir / line.replace(INCLUDE_MARK, "").strip()
|
1083
|
+
logging.debug(f"Including {fn} in {fpath}")
|
1084
|
+
source_lines.extend([f"\n// Included from {fn}\n\n"])
|
1085
|
+
with open(fn) as include:
|
1086
|
+
source_lines.extend(include.readlines())
|
1087
|
+
else:
|
1088
|
+
source_lines.append(line)
|
1089
|
+
|
1090
|
+
full_text = "".join(source_lines)
|
1091
|
+
|
1092
|
+
logging.debug(f"Loaded {fpath}")
|
1093
|
+
if log_path:
|
1094
|
+
logged_path = f"{log_path / fpath.name}_log"
|
1095
|
+
with open(logged_path, "w") as logged:
|
1096
|
+
logged.write(full_text)
|
1097
|
+
else:
|
1098
|
+
logged_path = None
|
1099
|
+
|
1100
|
+
shader_id = self.load_shader_from_source(shader_type, full_text)
|
1101
|
+
assert shader_dir not in self.shaders_names, "The shader is new, so it must be unknown to us"
|
1102
|
+
self.shaders_names[shader_id] = fpath.name
|
1103
|
+
return shader_id
|
1104
|
+
except Exception as ex:
|
1105
|
+
if logged_path is not None:
|
1106
|
+
lm = f"Log file at {logged_path}"
|
1107
|
+
else:
|
1108
|
+
lm = ""
|
1109
|
+
raise Exception(f"Error while loading {fpath}. {lm}") from ex
|
1110
|
+
|
1111
|
+
|
1112
|
+
def create_frame_buffer_for_computation(self,
|
1113
|
+
destination_texture:Union[int,list[int]],
|
1114
|
+
out_to_fragdata:bool=False,
|
1115
|
+
depth_buffer:bool=False,
|
1116
|
+
texture_target=GL_TEXTURE_RECTANGLE,
|
1117
|
+
):
|
1118
|
+
"""
|
1119
|
+
destination_texture: a texture or a list of textures. If list of textures,
|
1120
|
+
one frame buffer will be attached to each texture, via
|
1121
|
+
COLOR_ATTACHMENT0,1,2,... (in list order)
|
1122
|
+
|
1123
|
+
out_to_fragdata: if the FB will be used as output of a shader that issues
|
1124
|
+
FragData instead of colors. In that case, we create link buffers so that
|
1125
|
+
will receive these FragData.
|
1126
|
+
|
1127
|
+
depth_buffer: attach a depth buffer to the framebuffer. None or pass in the
|
1128
|
+
dimensions of the buffer. # FIXME don't pass the dimensions, guess them from
|
1129
|
+
the texture.
|
1130
|
+
|
1131
|
+
"""
|
1132
|
+
|
1133
|
+
# texture is where the result of the render will be
|
1134
|
+
# the stencil and depth information will be stored in a
|
1135
|
+
# RenderBuffer (which we don't make available here)
|
1136
|
+
|
1137
|
+
fb = glGenFramebuffers(1)
|
1138
|
+
glBindFramebuffer(GL_FRAMEBUFFER, fb)
|
1139
|
+
|
1140
|
+
# Wire the destination texture(s)
|
1141
|
+
if type(destination_texture) == list:
|
1142
|
+
assert len(destination_texture) <= len(
|
1143
|
+
GL_COLOR_ATTACHMENTS
|
1144
|
+
), "The GL_COLOR_ATTACHMENTS predefined values are not numerous enough !"
|
1145
|
+
|
1146
|
+
for i in range(len(destination_texture)):
|
1147
|
+
assert (
|
1148
|
+
destination_texture[i] is not None
|
1149
|
+
), f"The {i+1}th texture in the list of textures is None ?!"
|
1150
|
+
assert (
|
1151
|
+
destination_texture[i] in self.textures_formats
|
1152
|
+
), f"The {i+1}th texture in the list of textures has not format defined. Was it correctly initialized ?"
|
1153
|
+
# We read from the cached texture formats.
|
1154
|
+
context, format = self.textures_formats[destination_texture[i]]
|
1155
|
+
# glFramebufferTexture2D: attach a texture image to a framebuffer object
|
1156
|
+
glFramebufferTexture2D(
|
1157
|
+
GL_FRAMEBUFFER,
|
1158
|
+
GL_COLOR_ATTACHMENTS[i],
|
1159
|
+
context,
|
1160
|
+
destination_texture[i],
|
1161
|
+
0,
|
1162
|
+
)
|
1163
|
+
|
1164
|
+
if out_to_fragdata:
|
1165
|
+
# FIXME CRITICAL Is this really needed ? When I read:
|
1166
|
+
# https://registry.khronos.org/OpenGL-Refpages/gl4/html/glDrawBuffers.xhtml
|
1167
|
+
# I get the impression that glDrawBuffers must be called when a shader
|
1168
|
+
# is in context (as glDrawBuffers will wire the shader to some buffer).
|
1169
|
+
# Therefore doing this here, without some shader context, is useless...
|
1170
|
+
|
1171
|
+
# Redirect FragData out's of fragment shader to the destination texture
|
1172
|
+
# (they're connected via GL_COLOR_ATTACHMENTx)
|
1173
|
+
# FragData[0] corresponds to GL_COLOR_ATTACHMENT0 which was set to destination_textures[0] above.
|
1174
|
+
# So the buffer we create here is quite small, it's just an array of pointers.
|
1175
|
+
|
1176
|
+
# FIXME Replace the complicated ctypes code below with the simpler one right below.
|
1177
|
+
# The complex expression just pass texture id's to glDrawBuffers
|
1178
|
+
# under one or more GL_COLOR_ATTACHMENT0,1,2,...
|
1179
|
+
# glDrawBuffers — Specifies a list of color buffers to be drawn into
|
1180
|
+
# drawBuffers = gl_ca[0:len(destination_texture)]
|
1181
|
+
drawBuffers = (ctypes.c_int32 * len(destination_texture))(
|
1182
|
+
*[int(ca) for ca in GL_COLOR_ATTACHMENTS[0 : len(destination_texture)]]
|
1183
|
+
)
|
1184
|
+
# print(drawBuffers)
|
1185
|
+
# print([int(ca) for ca in gl_ca[0:len(destination_texture)]])
|
1186
|
+
glDrawBuffers(
|
1187
|
+
drawBuffers
|
1188
|
+
) # pyOpenGl figures the count based on drawBuffers array length
|
1189
|
+
else:
|
1190
|
+
assert destination_texture is not None, "Null texture id ???"
|
1191
|
+
assert type(destination_texture) in (
|
1192
|
+
np.uintc,
|
1193
|
+
int,
|
1194
|
+
), f"I want an integer texture id (you gave '{type(destination_texture)}')"
|
1195
|
+
glFramebufferTexture2D(
|
1196
|
+
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture_target, destination_texture, 0
|
1197
|
+
)
|
1198
|
+
|
1199
|
+
if not out_to_fragdata:
|
1200
|
+
glDrawBuffers(GL_NONE)
|
1201
|
+
# glReadBuffer(GL_NONE) # FIXME Maybe useless
|
1202
|
+
|
1203
|
+
if depth_buffer:
|
1204
|
+
if True:
|
1205
|
+
# see http://www.songho.ca/opengl/gl_fbo.html
|
1206
|
+
rb_id = glGenRenderbuffers(1) # create one render buffer
|
1207
|
+
glBindRenderbuffer(GL_RENDERBUFFER, rb_id)
|
1208
|
+
glRenderbufferStorage(
|
1209
|
+
GL_RENDERBUFFER, GL_DEPTH_COMPONENT32F, depth_buffer[0], depth_buffer[1]
|
1210
|
+
)
|
1211
|
+
glBindRenderbuffer(GL_RENDERBUFFER, 0) # unbind
|
1212
|
+
# Attach to currently bound F.B.
|
1213
|
+
glFramebufferRenderbuffer(
|
1214
|
+
GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rb_id
|
1215
|
+
)
|
1216
|
+
else:
|
1217
|
+
pass
|
1218
|
+
assert glGetError() == GL_NO_ERROR
|
1219
|
+
|
1220
|
+
assert glGetError() == GL_NO_ERROR
|
1221
|
+
# GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT=_C('GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT',0x8CD6)
|
1222
|
+
assert glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, f"Framebuffer is not complete, it is 0x{glCheckFramebufferStatus(GL_FRAMEBUFFER):04X}"
|
1223
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
1224
|
+
|
1225
|
+
return fb
|
1226
|
+
|
1227
|
+
|
1228
|
+
def load_program(self,
|
1229
|
+
vertex_shader:int=None,
|
1230
|
+
fragment_shader:int=None,
|
1231
|
+
geometry_shader:int=None,
|
1232
|
+
compute_shader:int=None
|
1233
|
+
):
|
1234
|
+
assert compute_shader is not None or vertex_shader is not None
|
1235
|
+
# returns the program
|
1236
|
+
|
1237
|
+
program = glCreateProgram()
|
1238
|
+
if program == 0:
|
1239
|
+
return 0
|
1240
|
+
|
1241
|
+
# At this point, shaders are compiled but not linked to each other
|
1242
|
+
|
1243
|
+
shaders = []
|
1244
|
+
if compute_shader is None:
|
1245
|
+
glAttachShader(program, vertex_shader)
|
1246
|
+
shaders.append(vertex_shader)
|
1247
|
+
if geometry_shader is not None:
|
1248
|
+
glAttachShader(program, geometry_shader)
|
1249
|
+
shaders.append(geometry_shader)
|
1250
|
+
if fragment_shader is not None:
|
1251
|
+
glAttachShader(program, fragment_shader)
|
1252
|
+
shaders.append(fragment_shader)
|
1253
|
+
else:
|
1254
|
+
# Compute shader
|
1255
|
+
glAttachShader(program, compute_shader)
|
1256
|
+
shaders.append(compute_shader)
|
1257
|
+
|
1258
|
+
glLinkProgram(program)
|
1259
|
+
|
1260
|
+
if glGetProgramiv(program, GL_LINK_STATUS, None) == GL_FALSE:
|
1261
|
+
glDeleteProgram(program)
|
1262
|
+
raise Exception("Failed to create a rpogram")
|
1263
|
+
|
1264
|
+
assert glGetError() == GL_NO_ERROR
|
1265
|
+
self.shaders_programs[program] = shaders
|
1266
|
+
|
1267
|
+
# Mark all shaders for removal once their corresponding programs will be
|
1268
|
+
# gone (read the documentation of glDeleteShader).
|
1269
|
+
# So be sure to do this *after* the shaders have been attached to their
|
1270
|
+
# program.
|
1271
|
+
for shader in shaders:
|
1272
|
+
glDeleteShader(shader)
|
1273
|
+
|
1274
|
+
return program
|
1275
|
+
|
1276
|
+
|
1277
|
+
|
1278
|
+
def set_uniform(self, program:int, name:str, value):
|
1279
|
+
# We cache the uniform settings because calls to `glUniform`
|
1280
|
+
# are super expensive (like *unbelievably* *expensive*)
|
1281
|
+
k = f"{program},{name}"
|
1282
|
+
if k in self._set_uniform_cache:
|
1283
|
+
current = self._set_uniform_cache[k]
|
1284
|
+
if isinstance(value, np.ndarray):
|
1285
|
+
if np.allclose(value, current):
|
1286
|
+
return
|
1287
|
+
elif current == value:
|
1288
|
+
return
|
1289
|
+
self._set_uniform_cache[k] = value
|
1290
|
+
|
1291
|
+
try:
|
1292
|
+
location = glGetUniformLocation(program, name)
|
1293
|
+
except Exception as ex:
|
1294
|
+
raise Exception(f"Can't get uniform location for '{name}'") from ex
|
1295
|
+
|
1296
|
+
if not location >= 0:
|
1297
|
+
logging.error(
|
1298
|
+
f"Can't find '{name}' uniform in *compiled* GL/SL program {program} (with shaders {[self.shaders_names[sid] for sid in self.shaders_programs[program]]}). Maybe you mixed uniforms and texture samplers ? Remember that shaders' compiler may remove uniforms that are not actually used in the GLSL code !"
|
1299
|
+
)
|
1300
|
+
return
|
1301
|
+
|
1302
|
+
# OpenGl: This (glGetUniformLocation) function returns -1 if name does not
|
1303
|
+
# correspond to an active uniform variable in program, if name starts with
|
1304
|
+
# the reserved prefix "gl_", or if name is associated with an atomic counter
|
1305
|
+
# or a named uniform block.
|
1306
|
+
assert (
|
1307
|
+
location >= 0
|
1308
|
+
), f"Can't find '{name}' uniform in *compiled* GL/SL program. Maybe you mixed uniforms and texture samplers ? Remember that shaders' compiler may remove uniforms that are not actually used in the GLSL code !"
|
1309
|
+
|
1310
|
+
if type(value) == float:
|
1311
|
+
glUniform1f(location, value)
|
1312
|
+
elif type(value) == bool:
|
1313
|
+
glUniform1ui(location, int(value))
|
1314
|
+
elif type(value) in (int, np.intc):
|
1315
|
+
try:
|
1316
|
+
#logging.debug(f"glUniform1i({name} as {location}, {value})")
|
1317
|
+
glUniform1i(location, value)
|
1318
|
+
except Exception as ex:
|
1319
|
+
logging.error(
|
1320
|
+
f"Error while setting integer uniform '{name}' at location '{location}' to value '{value}' (of type {type(value)})"
|
1321
|
+
)
|
1322
|
+
raise ex
|
1323
|
+
elif type(value) == np.uintc: # A 32 bit (uint32 is "at least 32 bits")
|
1324
|
+
try:
|
1325
|
+
glUniform1ui(location, value)
|
1326
|
+
except Exception as ex:
|
1327
|
+
logging.error(
|
1328
|
+
f"Error while setting integer uniform '{name}' at location '{location}' to value '{value}' (of type {type(value)})"
|
1329
|
+
)
|
1330
|
+
raise ex
|
1331
|
+
elif (
|
1332
|
+
type(value) in (tuple, list)
|
1333
|
+
and len(value) == 2
|
1334
|
+
and type(value[0]) == float
|
1335
|
+
and type(value[1]) == float
|
1336
|
+
):
|
1337
|
+
glUniform2f(location, value[0], value[1])
|
1338
|
+
elif isinstance(value, np.ndarray) and value.shape == (4, 4):
|
1339
|
+
# Attention! OpenGL matrices are column-major
|
1340
|
+
# At this point of the code we don't make any hypothesis
|
1341
|
+
# about the ordering of data in the numpy array => the caller
|
1342
|
+
# must ensure the OpenGL order (column major).
|
1343
|
+
|
1344
|
+
# glUniformMatrix4fv( loc, count, transpose, value)
|
1345
|
+
glUniformMatrix4fv(location, 1, GL_FALSE, value) # GL_FALSE= do not transpose
|
1346
|
+
else:
|
1347
|
+
if type(value) in (tuple, list):
|
1348
|
+
raise Exception(f"Error while setting uniform '{name}' at location '{location}' to value '{value}': Unsupported type: {type(value)} or types of parts are not supported.")
|
1349
|
+
else:
|
1350
|
+
raise Exception(f"Error while setting uniform '{name}' at location '{location}' to value '{value}': Unsupported type: {type(value)}")
|
1351
|
+
assert glGetError() == GL_NO_ERROR
|
1352
|
+
|
1353
|
+
|
1354
|
+
def set_texture(self, program:int, unif_name:str, tex_index:int, tex_unit):
|
1355
|
+
glActiveTexture(TEXTURE_UNITS[tex_unit])
|
1356
|
+
glBindTexture(GL_TEXTURE_RECTANGLE, tex_index)
|
1357
|
+
self.set_uniform(program, unif_name, tex_unit)
|
1358
|
+
|
1359
|
+
|
1360
|
+
def wire_program(self, program: int, uniforms=dict(), textures=dict()):
|
1361
|
+
"""
|
1362
|
+
Binds texture to (read) sampler.
|
1363
|
+
Sets uniforms.
|
1364
|
+
This doesn't touch the framebuffer bindings.
|
1365
|
+
|
1366
|
+
`program` : OpenGL id of the program
|
1367
|
+
`uniforms`: map uniform names (str) to their values
|
1368
|
+
`textures`: map texture sampler names to their texture id. Instead of texture_id you can
|
1369
|
+
pass a tuple (texture_id, access) where access is either: GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE.
|
1370
|
+
See https://www.khronos.org/opengl/wiki/Image_Load_Store
|
1371
|
+
"""
|
1372
|
+
|
1373
|
+
# logging.debug(f"Wiring program {textures}")
|
1374
|
+
assert glGetIntegerv(GL_CURRENT_PROGRAM) == program, "Seems like you program is no glUseProgram'ed"
|
1375
|
+
|
1376
|
+
# Wire uniforms
|
1377
|
+
for unif_name, unif_value in uniforms.items():
|
1378
|
+
self.set_uniform(program, unif_name, unif_value)
|
1379
|
+
|
1380
|
+
# Wire the texture samplers to texture units
|
1381
|
+
# We arbitrarily set the texture unit
|
1382
|
+
texture_unit = texture_image_unit = 0
|
1383
|
+
for sampler_name, tex_index in textures.items():
|
1384
|
+
if type(tex_index) == tuple:
|
1385
|
+
# We wire an image texture, with a image unit (instead of a texture
|
1386
|
+
# with a texture unit)
|
1387
|
+
tex_index, access = tex_index
|
1388
|
+
else:
|
1389
|
+
access = GL_READ_ONLY
|
1390
|
+
|
1391
|
+
assert type(tex_index) in (
|
1392
|
+
int,
|
1393
|
+
np.uintc,
|
1394
|
+
), f"Texture ID must be integer (sampler='{sampler_name}'). You gave {type(tex_index)}."
|
1395
|
+
|
1396
|
+
# Check that textures of a given type are compatible with their
|
1397
|
+
# sampler's type (for example a *u*sampler will make sense
|
1398
|
+
# only on a uint texture).
|
1399
|
+
# This is done because OpenGL is completely silent on these
|
1400
|
+
# mesimatches and it makes debugging very difficult.
|
1401
|
+
|
1402
|
+
# Check all shaders associated with the `program`
|
1403
|
+
sampler_found = False
|
1404
|
+
context = None
|
1405
|
+
for shader in self.shaders_programs[program]:
|
1406
|
+
k = (shader, sampler_name)
|
1407
|
+
if k in self.samplers_in_shader:
|
1408
|
+
image_texture = None
|
1409
|
+
sampler_type = self.samplers_in_shader[k]
|
1410
|
+
context, format = self.textures_formats[tex_index]
|
1411
|
+
msg = f"Wiring sampler '{sampler_name}' of type '{sampler_type}' to shader {shader} in program {program} to a texture of context {context} seems wrong. You gave {format} which looks incompatible/unsupported."
|
1412
|
+
if sampler_type == "usampler2DRect":
|
1413
|
+
assert (
|
1414
|
+
context == GL_TEXTURE_RECTANGLE
|
1415
|
+
), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
|
1416
|
+
assert format in (GL_R32UI, GL_R8UI, GL_RGBA8, GL_RG16UI, GL_RGB16UI, GL_RGB32UI), msg
|
1417
|
+
image_texture = False
|
1418
|
+
elif sampler_type == "sampler2DRect":
|
1419
|
+
assert (
|
1420
|
+
context == GL_TEXTURE_RECTANGLE
|
1421
|
+
), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
|
1422
|
+
assert format in (GL_R32F, GL_RGB32F, GL_RGBA32F), msg
|
1423
|
+
image_texture = False
|
1424
|
+
elif sampler_type == "isampler2DRect":
|
1425
|
+
assert (
|
1426
|
+
context == GL_TEXTURE_RECTANGLE
|
1427
|
+
), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
|
1428
|
+
assert format in (GL_R32I,), msg
|
1429
|
+
image_texture = False
|
1430
|
+
elif sampler_type == "image2D":
|
1431
|
+
# This is introduced to support compute shaders
|
1432
|
+
assert (
|
1433
|
+
context == GL_TEXTURE_2D
|
1434
|
+
), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_2D context"
|
1435
|
+
assert format in (GL_RGBA32F,), msg
|
1436
|
+
image_texture = True
|
1437
|
+
elif sampler_type == "image2DRect":
|
1438
|
+
# This is introduced to support compute shaders
|
1439
|
+
assert (
|
1440
|
+
context == GL_TEXTURE_RECTANGLE
|
1441
|
+
), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
|
1442
|
+
assert format in (GL_RGBA32F, GL_RGB32F, GL_R32F, GL_RG32F), msg
|
1443
|
+
image_texture = True
|
1444
|
+
elif sampler_type == "uimage2DRect":
|
1445
|
+
# This is introduced to support compute shaders
|
1446
|
+
assert (
|
1447
|
+
context == GL_TEXTURE_RECTANGLE
|
1448
|
+
), f"For sampler '{sampler_type}', OpenGL expects a GL_TEXTURE_RECTANGLE context"
|
1449
|
+
assert format in (GL_R8UI, GL_RGBA32UI, GL_RG16UI), msg
|
1450
|
+
image_texture = True
|
1451
|
+
else:
|
1452
|
+
raise Exception(f"Unsupported sampler type: {sampler_type}")
|
1453
|
+
sampler_found = True
|
1454
|
+
logging.debug(
|
1455
|
+
f"Ready to wire {['Texture','ImageTexture'][image_texture]} number {tex_index} on sampler '{sampler_name}' of type '{sampler_type}' to shader {self.shaders_names[shader]} ({shader}) in program {program} to a texture of context {context}. Format is {format}"
|
1456
|
+
)
|
1457
|
+
break
|
1458
|
+
|
1459
|
+
assert (
|
1460
|
+
sampler_found
|
1461
|
+
), f"Wiring program: Unknown sampler '{sampler_name}' in program {program} ({self.describe_program(program)})"
|
1462
|
+
|
1463
|
+
# How to bind texture and images is described here :
|
1464
|
+
# https://www.khronos.org/opengl/wiki/Texture#GLSL_binding
|
1465
|
+
|
1466
|
+
assert image_texture is not None
|
1467
|
+
if not image_texture:
|
1468
|
+
# glActiveTexture selects which texture unit subsequent texture state
|
1469
|
+
# calls will affect.
|
1470
|
+
glActiveTexture(TEXTURE_UNITS[texture_unit])
|
1471
|
+
|
1472
|
+
# Bind texture to active texture unit
|
1473
|
+
glBindTexture(context, tex_index)
|
1474
|
+
|
1475
|
+
logging.debug(f"glActiveTexture(texture_unit={TEXTURE_UNITS[texture_unit]}); glBindTexture({context}, texture_name={tex_index})")
|
1476
|
+
|
1477
|
+
# Tell the shader to use the texture unit `tex_unit` which we just
|
1478
|
+
# have wired a texture to.
|
1479
|
+
self.set_uniform(program, sampler_name, texture_unit)
|
1480
|
+
texture_unit += 1
|
1481
|
+
|
1482
|
+
elif image_texture:
|
1483
|
+
# https://stackoverflow.com/questions/37136813/what-is-the-difference-between-glbindimagetexture-and-glbindtexture
|
1484
|
+
# Right now this is used for compute shaders.
|
1485
|
+
# From: https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction
|
1486
|
+
# Here the glBindImageTexture function is used to bind a specific level of a texture to an image unit.
|
1487
|
+
|
1488
|
+
# So one can have a single texture associated to one texture unit
|
1489
|
+
# and several images linked to that texture unit, each associated to
|
1490
|
+
# one image unit. (there's one level of indirection when compared to
|
1491
|
+
# the usual texture/texture unit connections).
|
1492
|
+
|
1493
|
+
# Now to simplify things, since we don't (so far) use sveral
|
1494
|
+
# layers of the same texture in different image unit, we'll
|
1495
|
+
# make a STRONG assumption: if one chooses an image unit "i"
|
1496
|
+
# then we automatically binds it to the texture unit "i".
|
1497
|
+
|
1498
|
+
# If we have three texture we want access to, we'll
|
1499
|
+
# need three image units. Each of them will be wired
|
1500
|
+
# to the corresponding texture unit.
|
1501
|
+
|
1502
|
+
# Given the current texture, binds a single image of it to
|
1503
|
+
# the shader program.
|
1504
|
+
logging.debug(
|
1505
|
+
f"glBindImageTexture( texunit= {texture_image_unit}, tex_id={tex_index}, level=0, layered=GL_FALSE, layer=0, {access}, {format})"
|
1506
|
+
)
|
1507
|
+
if format == GL_RGB32F:
|
1508
|
+
logging.error(
|
1509
|
+
"ImageTexture can't be GL_RGB32F. See OpengGL documentation."
|
1510
|
+
)
|
1511
|
+
|
1512
|
+
# Bind the texture tex_index to the texture unit texture_image_unit.
|
1513
|
+
glBindImageTexture(
|
1514
|
+
texture_image_unit, tex_index, 0, GL_FALSE, 0, access, format
|
1515
|
+
)
|
1516
|
+
|
1517
|
+
|
1518
|
+
# It's not done in the OpenGl tutorial here : https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction
|
1519
|
+
# But according to the official doc here : https://www.khronos.org/opengl/wiki/Texture#GLSL_binding
|
1520
|
+
# it should be done too...
|
1521
|
+
self.set_uniform(program, sampler_name, texture_image_unit)
|
1522
|
+
#texture_image_unit += 1
|
1523
|
+
texture_image_unit += 1
|
1524
|
+
|
1525
|
+
mtu = glGetInteger(GL_MAX_TEXTURE_IMAGE_UNITS)
|
1526
|
+
# FIXME I'm absolutely NOT sure of this test.
|
1527
|
+
# First question are texture_image_unit and textre_unit the same thing ???
|
1528
|
+
if texture_unit + texture_image_unit > mtu - 1:
|
1529
|
+
raise Exception(f"Not enough TEXTURE_UNITS in gl (max is {mtu})")
|
1530
|
+
|
1531
|
+
|
1532
|
+
def init_gl(width, height):
|
1533
|
+
""" Initialize the OpenGL context and create a window with pygame """
|
1534
|
+
pygame.init()
|
1535
|
+
|
1536
|
+
display_nfo = pygame.display.Info()
|
1537
|
+
res = pygame.display.set_mode((width, height), pygame.DOUBLEBUF | pygame.OPENGL)
|
1538
|
+
return res
|
1539
|
+
|
1540
|
+
|
1541
|
+
if __name__ == "__main__":
|
1542
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
1543
|
+
init_gl(256, 256)
|
1544
|
+
query_gl_caps()
|