glplot 0.1.0__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.
- glplot/__init__.py +6 -0
- glplot/backend.py +15 -0
- glplot/controllers.py +69 -0
- glplot/core/__init__.py +0 -0
- glplot/core/context.py +50 -0
- glplot/core/layers.py +163 -0
- glplot/core/legacy.py +98 -0
- glplot/engine.py +1270 -0
- glplot/managers/__init__.py +0 -0
- glplot/managers/axis.py +66 -0
- glplot/managers/effects.py +343 -0
- glplot/managers/hud.py +510 -0
- glplot/managers/hud_state.py +95 -0
- glplot/managers/picking.py +174 -0
- glplot/managers/renderer_manager.py +158 -0
- glplot/options.py +120 -0
- glplot/policy.py +108 -0
- glplot/pyplot.py +735 -0
- glplot/renderers/__init__.py +0 -0
- glplot/renderers/axis.py +126 -0
- glplot/renderers/base.py +40 -0
- glplot/renderers/density.py +120 -0
- glplot/renderers/exact.py +215 -0
- glplot/renderers/interaction.py +77 -0
- glplot/renderers/line_family.py +250 -0
- glplot/renderers/patch.py +149 -0
- glplot/renderers/polyline.py +230 -0
- glplot/renderers/scatter.py +185 -0
- glplot/renderers/text.py +72 -0
- glplot/scratch/__init__.py +0 -0
- glplot/utils/__init__.py +0 -0
- glplot/utils/export.py +112 -0
- glplot/utils/gl_utils.py +32 -0
- glplot/utils/mpl_bridge.py +60 -0
- glplot/utils/shaders.py +889 -0
- glplot-0.1.0.dist-info/METADATA +75 -0
- glplot-0.1.0.dist-info/RECORD +39 -0
- glplot-0.1.0.dist-info/WHEEL +4 -0
- glplot-0.1.0.dist-info/licenses/LICENSE +0 -0
|
File without changes
|
glplot/managers/axis.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List, Tuple, Optional, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..engine import GPULinePlot
|
|
8
|
+
from ..core.context import RenderContext
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AxisTicks:
|
|
12
|
+
major: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
13
|
+
labels: List[str] = field(default_factory=list)
|
|
14
|
+
|
|
15
|
+
class AxisManager:
|
|
16
|
+
"""
|
|
17
|
+
Logical manager for coordinate axes.
|
|
18
|
+
Generates 'nice' ticks and labels based on the visible data range.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, plot: GPULinePlot):
|
|
21
|
+
self.plot = plot
|
|
22
|
+
self.options = plot.options
|
|
23
|
+
self.ticks_x = AxisTicks()
|
|
24
|
+
self.ticks_y = AxisTicks()
|
|
25
|
+
|
|
26
|
+
def update(self, ctx: RenderContext) -> None:
|
|
27
|
+
"""Recalculate ticks for the current view."""
|
|
28
|
+
# Target ~7 ticks for X, ~6 for Y as suggested by user
|
|
29
|
+
target_x = max(4, int(ctx.width_px / 160))
|
|
30
|
+
target_y = max(4, int(ctx.height_px / 120))
|
|
31
|
+
|
|
32
|
+
win = ctx.window_world
|
|
33
|
+
self.ticks_x = self._generate_ticks(win[0], win[1], target_x)
|
|
34
|
+
self.ticks_y = self._generate_ticks(win[2], win[3], target_y)
|
|
35
|
+
|
|
36
|
+
def _generate_ticks(self, vmin: float, vmax: float, target_count: int) -> AxisTicks:
|
|
37
|
+
if vmin >= vmax: return AxisTicks()
|
|
38
|
+
|
|
39
|
+
span = vmax - vmin
|
|
40
|
+
raw_step = span / max(1, target_count)
|
|
41
|
+
|
|
42
|
+
# Nice Step Algorithm (1, 2, 5 x 10^n)
|
|
43
|
+
p = 10 ** np.floor(np.log10(raw_step))
|
|
44
|
+
m = raw_step / p
|
|
45
|
+
|
|
46
|
+
if m < 1.5: step = 1.0 * p
|
|
47
|
+
elif m < 3.5: step = 2.0 * p
|
|
48
|
+
elif m < 7.5: step = 5.0 * p
|
|
49
|
+
else: step = 10.0 * p
|
|
50
|
+
|
|
51
|
+
# Calculate start/end
|
|
52
|
+
start = np.ceil(vmin / step) * step
|
|
53
|
+
end = np.floor(vmax / step) * step
|
|
54
|
+
|
|
55
|
+
if start > end: return AxisTicks()
|
|
56
|
+
|
|
57
|
+
major = np.arange(start, end + step/2, step)
|
|
58
|
+
|
|
59
|
+
# Generate labels
|
|
60
|
+
labels = []
|
|
61
|
+
precision = max(0, int(-np.floor(np.log10(step)))) if step < 1 else 0
|
|
62
|
+
fmt = f"{{:.{precision}f}}"
|
|
63
|
+
for v in major:
|
|
64
|
+
labels.append(fmt.format(v))
|
|
65
|
+
|
|
66
|
+
return AxisTicks(major=major, labels=labels)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import glfw
|
|
4
|
+
from OpenGL.GL import *
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..utils.gl_utils import link_program
|
|
8
|
+
from ..utils.shaders import (
|
|
9
|
+
POST_FX_VS,
|
|
10
|
+
GRADIENT_BG_FS,
|
|
11
|
+
BLOOM_EXTRACT_FS,
|
|
12
|
+
GAUSSIAN_BLUR_FS,
|
|
13
|
+
POST_COMPOSITE_FS,
|
|
14
|
+
)
|
|
15
|
+
from ..renderers.base import GLOffscreenTarget
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..engine import GPULinePlot
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EffectManager:
|
|
22
|
+
"""
|
|
23
|
+
Post-processing manager.
|
|
24
|
+
|
|
25
|
+
Design goals:
|
|
26
|
+
- Zero meaningful overhead when all effects are disabled.
|
|
27
|
+
- Lazy initialization of shaders/FBOs.
|
|
28
|
+
- Explicit scene render flow:
|
|
29
|
+
begin_scene()
|
|
30
|
+
draw_background()
|
|
31
|
+
<draw scene here>
|
|
32
|
+
end_scene()
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, plot: "GPULinePlot"):
|
|
36
|
+
self.plot = plot
|
|
37
|
+
self.options = plot.options
|
|
38
|
+
|
|
39
|
+
# Programs
|
|
40
|
+
self.prog_bg = 0
|
|
41
|
+
self.prog_extract = 0
|
|
42
|
+
self.prog_blur = 0
|
|
43
|
+
self.prog_composite = 0
|
|
44
|
+
|
|
45
|
+
# Cached uniform locations
|
|
46
|
+
self.u_bg_top_color = -1
|
|
47
|
+
self.u_bg_bottom_color = -1
|
|
48
|
+
|
|
49
|
+
self.u_extract_tex = -1
|
|
50
|
+
self.u_extract_threshold = -1
|
|
51
|
+
|
|
52
|
+
self.u_blur_tex = -1
|
|
53
|
+
self.u_blur_horizontal = -1
|
|
54
|
+
self.u_blur_radius = -1
|
|
55
|
+
|
|
56
|
+
self.u_comp_scene_tex = -1
|
|
57
|
+
self.u_comp_bloom_tex = -1
|
|
58
|
+
self.u_comp_bloom_enabled = -1
|
|
59
|
+
self.u_comp_bloom_intensity = -1
|
|
60
|
+
|
|
61
|
+
# FBOs
|
|
62
|
+
self.scene_fbo = GLOffscreenTarget()
|
|
63
|
+
self.extract_fbo = GLOffscreenTarget()
|
|
64
|
+
self.ping_fbo = GLOffscreenTarget()
|
|
65
|
+
self.pong_fbo = GLOffscreenTarget()
|
|
66
|
+
|
|
67
|
+
# Fullscreen quad
|
|
68
|
+
self.quad_vao = 0
|
|
69
|
+
|
|
70
|
+
self.initialized = False
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Public state
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def any_post_enabled(self) -> bool:
|
|
77
|
+
v = self.options.visual
|
|
78
|
+
return v.glow.enabled
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Lifecycle
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def ensure_resources(self) -> None:
|
|
85
|
+
if self.initialized:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
self.prog_bg = link_program(POST_FX_VS, GRADIENT_BG_FS)
|
|
89
|
+
self.prog_extract = link_program(POST_FX_VS, BLOOM_EXTRACT_FS)
|
|
90
|
+
self.prog_blur = link_program(POST_FX_VS, GAUSSIAN_BLUR_FS)
|
|
91
|
+
self.prog_composite = link_program(POST_FX_VS, POST_COMPOSITE_FS)
|
|
92
|
+
|
|
93
|
+
self.u_bg_top_color = glGetUniformLocation(self.prog_bg, "u_top_color")
|
|
94
|
+
self.u_bg_bottom_color = glGetUniformLocation(self.prog_bg, "u_bottom_color")
|
|
95
|
+
|
|
96
|
+
self.u_extract_tex = glGetUniformLocation(self.prog_extract, "u_tex")
|
|
97
|
+
self.u_extract_threshold = glGetUniformLocation(self.prog_extract, "u_threshold")
|
|
98
|
+
|
|
99
|
+
self.u_blur_tex = glGetUniformLocation(self.prog_blur, "u_tex")
|
|
100
|
+
self.u_blur_horizontal = glGetUniformLocation(self.prog_blur, "u_horizontal")
|
|
101
|
+
self.u_blur_radius = glGetUniformLocation(self.prog_blur, "u_radius")
|
|
102
|
+
|
|
103
|
+
self.u_comp_scene_tex = glGetUniformLocation(self.prog_composite, "u_scene_tex")
|
|
104
|
+
self.u_comp_bloom_tex = glGetUniformLocation(self.prog_composite, "u_bloom_tex")
|
|
105
|
+
self.u_comp_bloom_enabled = glGetUniformLocation(self.prog_composite, "u_bloom_enabled")
|
|
106
|
+
self.u_comp_bloom_intensity = glGetUniformLocation(self.prog_composite, "u_bloom_intensity")
|
|
107
|
+
|
|
108
|
+
self.quad_vao = glGenVertexArrays(1)
|
|
109
|
+
self._rebuild_targets()
|
|
110
|
+
self.initialized = True
|
|
111
|
+
|
|
112
|
+
def shutdown(self) -> None:
|
|
113
|
+
self._destroy_target(self.scene_fbo)
|
|
114
|
+
self._destroy_target(self.extract_fbo)
|
|
115
|
+
self._destroy_target(self.ping_fbo)
|
|
116
|
+
self._destroy_target(self.pong_fbo)
|
|
117
|
+
|
|
118
|
+
if self.quad_vao:
|
|
119
|
+
glDeleteVertexArrays(1, [self.quad_vao])
|
|
120
|
+
self.quad_vao = 0
|
|
121
|
+
|
|
122
|
+
for prog in (self.prog_bg, self.prog_extract, self.prog_blur, self.prog_composite):
|
|
123
|
+
if prog:
|
|
124
|
+
glDeleteProgram(prog)
|
|
125
|
+
|
|
126
|
+
self.prog_bg = 0
|
|
127
|
+
self.prog_extract = 0
|
|
128
|
+
self.prog_blur = 0
|
|
129
|
+
self.prog_composite = 0
|
|
130
|
+
self.initialized = False
|
|
131
|
+
|
|
132
|
+
def on_resize(self) -> None:
|
|
133
|
+
if self.initialized:
|
|
134
|
+
self._rebuild_targets()
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Scene flow
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def begin_scene(self) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Bind the correct render target for the scene.
|
|
143
|
+
"""
|
|
144
|
+
if not self.any_post_enabled():
|
|
145
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
146
|
+
glViewport(0, 0, self.plot.fb_width, self.plot.fb_height)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
self.ensure_resources()
|
|
150
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.scene_fbo.fbo)
|
|
151
|
+
glViewport(0, 0, self.scene_fbo.width, self.scene_fbo.height)
|
|
152
|
+
glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
153
|
+
glClear(GL_COLOR_BUFFER_BIT)
|
|
154
|
+
|
|
155
|
+
def end_scene(self) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Resolve post-processing if needed.
|
|
158
|
+
"""
|
|
159
|
+
if self.any_post_enabled():
|
|
160
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
161
|
+
self.resolve()
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
# Background
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def draw_background(self) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Draw the background into whichever framebuffer is currently bound.
|
|
170
|
+
"""
|
|
171
|
+
v = self.options.visual.gradient_background
|
|
172
|
+
|
|
173
|
+
if not v.enabled:
|
|
174
|
+
c = self.options.visual.background_color
|
|
175
|
+
glClearColor(c[0], c[1], c[2], 1.0)
|
|
176
|
+
glClear(GL_COLOR_BUFFER_BIT)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
self.ensure_resources()
|
|
180
|
+
glDisable(GL_BLEND)
|
|
181
|
+
|
|
182
|
+
# Disable world clipping for background pass
|
|
183
|
+
if self.options.enable_clipping_optimization:
|
|
184
|
+
for i in range(4): glDisable(GL_CLIP_DISTANCE0 + i)
|
|
185
|
+
|
|
186
|
+
glUseProgram(self.prog_bg)
|
|
187
|
+
glUniform3f(self.u_bg_top_color, *v.top_color)
|
|
188
|
+
glUniform3f(self.u_bg_bottom_color, *v.bottom_color)
|
|
189
|
+
|
|
190
|
+
glBindVertexArray(self.quad_vao)
|
|
191
|
+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
|
|
192
|
+
glBindVertexArray(0)
|
|
193
|
+
glUseProgram(0)
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# Post-processing
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def resolve(self) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Perform post-processing and draw to the default framebuffer.
|
|
202
|
+
"""
|
|
203
|
+
v = self.options.visual
|
|
204
|
+
if not self.any_post_enabled():
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
self.ensure_resources()
|
|
208
|
+
|
|
209
|
+
glDisable(GL_BLEND)
|
|
210
|
+
glBindVertexArray(self.quad_vao)
|
|
211
|
+
|
|
212
|
+
# 1) Bright-pass extraction
|
|
213
|
+
if v.glow.enabled:
|
|
214
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.extract_fbo.fbo)
|
|
215
|
+
glViewport(0, 0, self.extract_fbo.width, self.extract_fbo.height)
|
|
216
|
+
glUseProgram(self.prog_extract)
|
|
217
|
+
|
|
218
|
+
glActiveTexture(GL_TEXTURE0)
|
|
219
|
+
glBindTexture(GL_TEXTURE_2D, self.scene_fbo.tex)
|
|
220
|
+
glUniform1i(self.u_extract_tex, 0)
|
|
221
|
+
glUniform1f(self.u_extract_threshold, v.glow.threshold)
|
|
222
|
+
|
|
223
|
+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
|
|
224
|
+
|
|
225
|
+
# 2) Horizontal blur
|
|
226
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.ping_fbo.fbo)
|
|
227
|
+
glViewport(0, 0, self.ping_fbo.width, self.ping_fbo.height)
|
|
228
|
+
glUseProgram(self.prog_blur)
|
|
229
|
+
|
|
230
|
+
glActiveTexture(GL_TEXTURE0)
|
|
231
|
+
glBindTexture(GL_TEXTURE_2D, self.extract_fbo.tex)
|
|
232
|
+
glUniform1i(self.u_blur_tex, 0)
|
|
233
|
+
glUniform1i(self.u_blur_horizontal, 1)
|
|
234
|
+
glUniform1f(self.u_blur_radius, v.glow.radius_px)
|
|
235
|
+
|
|
236
|
+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
|
|
237
|
+
|
|
238
|
+
# 3) Vertical blur
|
|
239
|
+
glBindFramebuffer(GL_FRAMEBUFFER, self.pong_fbo.fbo)
|
|
240
|
+
glViewport(0, 0, self.pong_fbo.width, self.pong_fbo.height)
|
|
241
|
+
|
|
242
|
+
glActiveTexture(GL_TEXTURE0)
|
|
243
|
+
glBindTexture(GL_TEXTURE_2D, self.ping_fbo.tex)
|
|
244
|
+
glUniform1i(self.u_blur_tex, 0)
|
|
245
|
+
glUniform1i(self.u_blur_horizontal, 0)
|
|
246
|
+
glUniform1f(self.u_blur_radius, v.glow.radius_px)
|
|
247
|
+
|
|
248
|
+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
|
|
249
|
+
|
|
250
|
+
# 4) Final composite to default framebuffer
|
|
251
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
252
|
+
glViewport(0, 0, self.plot.fb_width, self.plot.fb_height)
|
|
253
|
+
glUseProgram(self.prog_composite)
|
|
254
|
+
|
|
255
|
+
glActiveTexture(GL_TEXTURE0)
|
|
256
|
+
glBindTexture(GL_TEXTURE_2D, self.scene_fbo.tex)
|
|
257
|
+
glUniform1i(self.u_comp_scene_tex, 0)
|
|
258
|
+
|
|
259
|
+
glActiveTexture(GL_TEXTURE1)
|
|
260
|
+
glBindTexture(GL_TEXTURE_2D, self.pong_fbo.tex if v.glow.enabled else self.scene_fbo.tex)
|
|
261
|
+
glUniform1i(self.u_comp_bloom_tex, 1)
|
|
262
|
+
|
|
263
|
+
glUniform1i(self.u_comp_bloom_enabled, 1 if v.glow.enabled else 0)
|
|
264
|
+
glUniform1f(self.u_comp_bloom_intensity, v.glow.intensity)
|
|
265
|
+
|
|
266
|
+
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
|
|
267
|
+
|
|
268
|
+
if self.options.enable_clipping_optimization:
|
|
269
|
+
# Re-enable for subsequent exact draws if they don't explicitly handle it
|
|
270
|
+
for i in range(4): glEnable(GL_CLIP_DISTANCE0 + i)
|
|
271
|
+
|
|
272
|
+
glBindVertexArray(0)
|
|
273
|
+
glUseProgram(0)
|
|
274
|
+
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
# Internal helpers
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
def _rebuild_targets(self) -> None:
|
|
280
|
+
w, h = self.plot.fb_width, self.plot.fb_height
|
|
281
|
+
if w <= 0 or h <= 0:
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
self._destroy_target(self.scene_fbo)
|
|
285
|
+
self._destroy_target(self.extract_fbo)
|
|
286
|
+
self._destroy_target(self.ping_fbo)
|
|
287
|
+
self._destroy_target(self.pong_fbo)
|
|
288
|
+
|
|
289
|
+
# Scene target: full res, float
|
|
290
|
+
self.scene_fbo = self._create_target(w, h, GL_RGBA16F)
|
|
291
|
+
|
|
292
|
+
# Bloom chain: half res
|
|
293
|
+
bw = max(1, int(round(w * 0.5)))
|
|
294
|
+
bh = max(1, int(round(h * 0.5)))
|
|
295
|
+
self.extract_fbo = self._create_target(bw, bh, GL_RGBA8)
|
|
296
|
+
self.ping_fbo = self._create_target(bw, bh, GL_RGBA8)
|
|
297
|
+
self.pong_fbo = self._create_target(bw, bh, GL_RGBA8)
|
|
298
|
+
|
|
299
|
+
def _create_target(self, w: int, h: int, internal_format: int) -> GLOffscreenTarget:
|
|
300
|
+
tex = glGenTextures(1)
|
|
301
|
+
glBindTexture(GL_TEXTURE_2D, tex)
|
|
302
|
+
|
|
303
|
+
if internal_format == GL_RGBA16F:
|
|
304
|
+
fmt = GL_RGBA
|
|
305
|
+
dtype = GL_FLOAT
|
|
306
|
+
elif internal_format == GL_RGBA8:
|
|
307
|
+
fmt = GL_RGBA
|
|
308
|
+
dtype = GL_UNSIGNED_BYTE
|
|
309
|
+
elif internal_format == GL_R32F:
|
|
310
|
+
fmt = GL_RED
|
|
311
|
+
dtype = GL_FLOAT
|
|
312
|
+
elif internal_format == GL_R32I:
|
|
313
|
+
fmt = GL_RED_INTEGER
|
|
314
|
+
dtype = GL_INT
|
|
315
|
+
else:
|
|
316
|
+
raise ValueError(f"Unsupported internal format: {internal_format}")
|
|
317
|
+
|
|
318
|
+
glTexImage2D(GL_TEXTURE_2D, 0, internal_format, w, h, 0, fmt, dtype, None)
|
|
319
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
|
320
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
|
321
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
|
322
|
+
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
|
323
|
+
|
|
324
|
+
fbo = glGenFramebuffers(1)
|
|
325
|
+
glBindFramebuffer(GL_FRAMEBUFFER, fbo)
|
|
326
|
+
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0)
|
|
327
|
+
|
|
328
|
+
status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
|
|
329
|
+
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
|
330
|
+
if status != GL_FRAMEBUFFER_COMPLETE:
|
|
331
|
+
raise RuntimeError(f"Framebuffer incomplete: {status}")
|
|
332
|
+
|
|
333
|
+
return GLOffscreenTarget(fbo=fbo, tex=tex, width=w, height=h)
|
|
334
|
+
|
|
335
|
+
def _destroy_target(self, target: GLOffscreenTarget) -> None:
|
|
336
|
+
if target.fbo:
|
|
337
|
+
glDeleteFramebuffers(1, [target.fbo])
|
|
338
|
+
if target.tex:
|
|
339
|
+
glDeleteTextures(1, [target.tex])
|
|
340
|
+
target.fbo = 0
|
|
341
|
+
target.tex = 0
|
|
342
|
+
target.width = 0
|
|
343
|
+
target.height = 0
|