vimol 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.
- vimol/__init__.py +63 -0
- vimol/__main__.py +4 -0
- vimol/app.py +218 -0
- vimol/arrows.py +91 -0
- vimol/bonds.py +79 -0
- vimol/camera.py +77 -0
- vimol/editor.py +646 -0
- vimol/elements.py +123 -0
- vimol/gl_adapter.py +114 -0
- vimol/gl_render.py +742 -0
- vimol/input.py +252 -0
- vimol/kitty.py +297 -0
- vimol/molecule.py +159 -0
- vimol/parsers/__init__.py +88 -0
- vimol/parsers/mol.py +57 -0
- vimol/parsers/pdb.py +97 -0
- vimol/parsers/xyz.py +68 -0
- vimol/periodic_table.py +97 -0
- vimol/py.typed +0 -0
- vimol/render.py +600 -0
- vimol/scene.py +239 -0
- vimol/templates.py +205 -0
- vimol/viewer.py +1159 -0
- vimol/widget.py +611 -0
- vimol-0.1.0.dist-info/METADATA +302 -0
- vimol-0.1.0.dist-info/RECORD +30 -0
- vimol-0.1.0.dist-info/WHEEL +5 -0
- vimol-0.1.0.dist-info/entry_points.txt +2 -0
- vimol-0.1.0.dist-info/licenses/LICENSE +20 -0
- vimol-0.1.0.dist-info/top_level.txt +1 -0
vimol/gl_render.py
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
"""Generic GPU sphere/cylinder impostor renderer (OpenGL, via moderngl).
|
|
2
|
+
|
|
3
|
+
This module knows nothing about molecules, atoms, bonds, or vimol's
|
|
4
|
+
``Camera``/``Style`` types. It renders batches of analytic spheres and
|
|
5
|
+
cylinders — the same "impostor" technique as the CPU raycaster in
|
|
6
|
+
``render.py`` (each primitive is a screen-aligned billboard whose fragment
|
|
7
|
+
shader solves the ray/surface intersection analytically), just executed on
|
|
8
|
+
the GPU with a real depth buffer instead of a manually-maintained z-buffer
|
|
9
|
+
array. Callers supply a raw 4x4 projection matrix and plain shading
|
|
10
|
+
parameters; nothing here is vimol-specific, so it's reusable outside this
|
|
11
|
+
project. See ``gl_adapter.py`` for the vimol-specific glue that turns a
|
|
12
|
+
``Molecule`` + ``Camera`` + ``Style`` into the inputs this module expects.
|
|
13
|
+
|
|
14
|
+
Requires the optional ``moderngl`` dependency (``pip install vimol[gl]``).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Tuple
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
import moderngl
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SphereBatch:
|
|
28
|
+
"""A batch of analytic spheres, structure-of-arrays style.
|
|
29
|
+
|
|
30
|
+
``centers``/``radii``/``colors`` must all be in the same coordinate
|
|
31
|
+
space as the ``proj`` matrix passed to :meth:`GLRenderer.render`
|
|
32
|
+
(typically view space, with the convention that larger Z is nearer the
|
|
33
|
+
viewer — see ``gl_adapter.py``).
|
|
34
|
+
"""
|
|
35
|
+
centers: np.ndarray # (N, 3) float
|
|
36
|
+
radii: np.ndarray # (N,) float
|
|
37
|
+
colors: np.ndarray # (N, 3) float, 0..1
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def empty() -> "SphereBatch":
|
|
41
|
+
return SphereBatch(
|
|
42
|
+
centers=np.zeros((0, 3), np.float32),
|
|
43
|
+
radii=np.zeros((0,), np.float32),
|
|
44
|
+
colors=np.zeros((0, 3), np.float32),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class CylinderBatch:
|
|
50
|
+
"""A batch of analytic cylinders (capped, but caps aren't drawn — bonds
|
|
51
|
+
are always occluded/occluding via the shared depth buffer instead)."""
|
|
52
|
+
a: np.ndarray # (M, 3) float — one endpoint
|
|
53
|
+
b: np.ndarray # (M, 3) float — other endpoint
|
|
54
|
+
radii: np.ndarray # (M,) float
|
|
55
|
+
colors_a: np.ndarray # (M, 3) float — color at the `a` end
|
|
56
|
+
colors_b: np.ndarray # (M, 3) float — color at the `b` end
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def empty() -> "CylinderBatch":
|
|
60
|
+
return CylinderBatch(
|
|
61
|
+
a=np.zeros((0, 3), np.float32),
|
|
62
|
+
b=np.zeros((0, 3), np.float32),
|
|
63
|
+
radii=np.zeros((0,), np.float32),
|
|
64
|
+
colors_a=np.zeros((0, 3), np.float32),
|
|
65
|
+
colors_b=np.zeros((0, 3), np.float32),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ConeBatch:
|
|
71
|
+
"""A batch of analytic cones (arrow heads): radius *radius* at ``base``,
|
|
72
|
+
tapering linearly to a point at ``apex``. Used for the arrowhead half of
|
|
73
|
+
a vector-field arrow; the shaft half is a plain ``CylinderBatch`` entry."""
|
|
74
|
+
base: np.ndarray # (K, 3) float
|
|
75
|
+
apex: np.ndarray # (K, 3) float
|
|
76
|
+
radius: np.ndarray # (K,) float
|
|
77
|
+
color: np.ndarray # (K, 3) float
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def empty() -> "ConeBatch":
|
|
81
|
+
return ConeBatch(
|
|
82
|
+
base=np.zeros((0, 3), np.float32),
|
|
83
|
+
apex=np.zeros((0, 3), np.float32),
|
|
84
|
+
radius=np.zeros((0,), np.float32),
|
|
85
|
+
color=np.zeros((0, 3), np.float32),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ShadingParams:
|
|
91
|
+
"""Plain Phong/fog shading parameters — no representation/chemistry
|
|
92
|
+
concepts, unlike ``render.Style``."""
|
|
93
|
+
ambient: float = 0.28
|
|
94
|
+
specular_strength: float = 0.55
|
|
95
|
+
shininess: float = 32.0
|
|
96
|
+
light_dir: Tuple[float, float, float] = (0.35, 0.55, 0.75)
|
|
97
|
+
fill_light: float = 0.25
|
|
98
|
+
depth_cue: float = 0.55
|
|
99
|
+
background: Tuple[float, float, float] = (0.05, 0.06, 0.09)
|
|
100
|
+
transparent: bool = False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_ATOM_VERT = """
|
|
104
|
+
#version 410 core
|
|
105
|
+
layout(location = 0) in vec2 in_corner;
|
|
106
|
+
layout(location = 1) in vec3 in_center;
|
|
107
|
+
layout(location = 2) in float in_radius;
|
|
108
|
+
layout(location = 3) in vec3 in_color;
|
|
109
|
+
|
|
110
|
+
uniform mat4 u_proj;
|
|
111
|
+
|
|
112
|
+
out vec2 v_offset;
|
|
113
|
+
out vec3 v_center;
|
|
114
|
+
out float v_radius;
|
|
115
|
+
out vec3 v_color;
|
|
116
|
+
|
|
117
|
+
void main() {
|
|
118
|
+
v_offset = in_corner * in_radius;
|
|
119
|
+
v_center = in_center;
|
|
120
|
+
v_radius = in_radius;
|
|
121
|
+
v_color = in_color;
|
|
122
|
+
gl_Position = u_proj * vec4(in_center.xy + v_offset, in_center.z, 1.0);
|
|
123
|
+
}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
_ATOM_FRAG = """
|
|
127
|
+
#version 410 core
|
|
128
|
+
in vec2 v_offset;
|
|
129
|
+
in vec3 v_center;
|
|
130
|
+
in float v_radius;
|
|
131
|
+
in vec3 v_color;
|
|
132
|
+
|
|
133
|
+
uniform float u_proj_z_scale;
|
|
134
|
+
uniform float u_proj_z_bias;
|
|
135
|
+
uniform vec3 u_light_dir;
|
|
136
|
+
uniform vec3 u_half_vec;
|
|
137
|
+
uniform vec3 u_fill_dir;
|
|
138
|
+
uniform float u_ambient;
|
|
139
|
+
uniform float u_fill_light;
|
|
140
|
+
uniform float u_specular_strength;
|
|
141
|
+
uniform float u_shininess;
|
|
142
|
+
uniform float u_depth_cue;
|
|
143
|
+
uniform float u_zmin;
|
|
144
|
+
uniform float u_zspan;
|
|
145
|
+
|
|
146
|
+
out vec4 frag_color;
|
|
147
|
+
|
|
148
|
+
void main() {
|
|
149
|
+
float ox = v_offset.x;
|
|
150
|
+
float oy = v_offset.y;
|
|
151
|
+
float r = v_radius;
|
|
152
|
+
float h2 = r * r - ox * ox - oy * oy;
|
|
153
|
+
if (h2 < 0.0) discard;
|
|
154
|
+
float h = sqrt(h2);
|
|
155
|
+
float depth_view = v_center.z + h;
|
|
156
|
+
vec3 normal = vec3(ox / r, oy / r, h / r);
|
|
157
|
+
|
|
158
|
+
float nl = clamp(dot(normal, u_light_dir), 0.0, 1.0);
|
|
159
|
+
float nf = clamp(dot(normal, u_fill_dir), 0.0, 1.0) * u_fill_light;
|
|
160
|
+
float nh = clamp(dot(normal, u_half_vec), 0.0, 1.0);
|
|
161
|
+
float spec = pow(nh, u_shininess) * u_specular_strength;
|
|
162
|
+
float diff = u_ambient + nl + nf;
|
|
163
|
+
vec3 shaded = v_color * diff + vec3(spec);
|
|
164
|
+
|
|
165
|
+
if (u_depth_cue > 0.0) {
|
|
166
|
+
float f = clamp((depth_view - u_zmin) / u_zspan, 0.0, 1.0);
|
|
167
|
+
float fog = 1.0 - u_depth_cue * (1.0 - f);
|
|
168
|
+
shaded *= fog;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
frag_color = vec4(shaded, 1.0);
|
|
172
|
+
float nz = u_proj_z_scale * depth_view + u_proj_z_bias;
|
|
173
|
+
gl_FragDepth = nz * 0.5 + 0.5;
|
|
174
|
+
}
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
_BOND_VERT = """
|
|
178
|
+
#version 410 core
|
|
179
|
+
layout(location = 0) in vec2 in_corner;
|
|
180
|
+
layout(location = 1) in vec3 in_a;
|
|
181
|
+
layout(location = 2) in vec3 in_b;
|
|
182
|
+
layout(location = 3) in float in_radius;
|
|
183
|
+
layout(location = 4) in vec3 in_color_a;
|
|
184
|
+
layout(location = 5) in vec3 in_color_b;
|
|
185
|
+
|
|
186
|
+
uniform mat4 u_proj;
|
|
187
|
+
|
|
188
|
+
out vec2 v_pixel;
|
|
189
|
+
out vec3 v_a;
|
|
190
|
+
out vec3 v_b;
|
|
191
|
+
out float v_radius;
|
|
192
|
+
out vec3 v_color_a;
|
|
193
|
+
out vec3 v_color_b;
|
|
194
|
+
|
|
195
|
+
void main() {
|
|
196
|
+
vec2 lo = min(in_a.xy, in_b.xy) - vec2(in_radius);
|
|
197
|
+
vec2 hi = max(in_a.xy, in_b.xy) + vec2(in_radius);
|
|
198
|
+
vec2 center = 0.5 * (lo + hi);
|
|
199
|
+
vec2 half_extent = max(0.5 * (hi - lo), vec2(1e-4));
|
|
200
|
+
vec2 pos = center + in_corner * half_extent;
|
|
201
|
+
|
|
202
|
+
v_pixel = pos;
|
|
203
|
+
v_a = in_a;
|
|
204
|
+
v_b = in_b;
|
|
205
|
+
v_radius = in_radius;
|
|
206
|
+
v_color_a = in_color_a;
|
|
207
|
+
v_color_b = in_color_b;
|
|
208
|
+
|
|
209
|
+
float z_mid = 0.5 * (in_a.z + in_b.z);
|
|
210
|
+
gl_Position = u_proj * vec4(pos, z_mid, 1.0);
|
|
211
|
+
}
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
_BOND_FRAG = """
|
|
215
|
+
#version 410 core
|
|
216
|
+
in vec2 v_pixel;
|
|
217
|
+
in vec3 v_a;
|
|
218
|
+
in vec3 v_b;
|
|
219
|
+
in float v_radius;
|
|
220
|
+
in vec3 v_color_a;
|
|
221
|
+
in vec3 v_color_b;
|
|
222
|
+
|
|
223
|
+
uniform float u_proj_z_scale;
|
|
224
|
+
uniform float u_proj_z_bias;
|
|
225
|
+
uniform vec3 u_light_dir;
|
|
226
|
+
uniform vec3 u_half_vec;
|
|
227
|
+
uniform vec3 u_fill_dir;
|
|
228
|
+
uniform float u_ambient;
|
|
229
|
+
uniform float u_fill_light;
|
|
230
|
+
uniform float u_specular_strength;
|
|
231
|
+
uniform float u_shininess;
|
|
232
|
+
uniform float u_depth_cue;
|
|
233
|
+
uniform float u_zmin;
|
|
234
|
+
uniform float u_zspan;
|
|
235
|
+
|
|
236
|
+
out vec4 frag_color;
|
|
237
|
+
|
|
238
|
+
void main() {
|
|
239
|
+
vec3 axis = v_b - v_a;
|
|
240
|
+
float L = length(axis);
|
|
241
|
+
if (L < 1e-6) discard;
|
|
242
|
+
vec3 u = axis / L;
|
|
243
|
+
|
|
244
|
+
float ex = v_pixel.x - v_a.x;
|
|
245
|
+
float ey = v_pixel.y - v_a.y;
|
|
246
|
+
float uz = u.z;
|
|
247
|
+
float A0 = ex * u.x + ey * u.y;
|
|
248
|
+
float a2 = 1.0 - uz * uz;
|
|
249
|
+
float c2 = ex * ex + ey * ey - A0 * A0 - v_radius * v_radius;
|
|
250
|
+
|
|
251
|
+
float s;
|
|
252
|
+
bool hit;
|
|
253
|
+
if (abs(a2) < 1e-9) {
|
|
254
|
+
hit = (-c2) >= 0.0;
|
|
255
|
+
s = 0.0;
|
|
256
|
+
} else {
|
|
257
|
+
float b2 = -2.0 * A0 * uz;
|
|
258
|
+
float disc = b2 * b2 - 4.0 * a2 * c2;
|
|
259
|
+
hit = disc >= 0.0;
|
|
260
|
+
float sq = sqrt(max(disc, 0.0));
|
|
261
|
+
s = (-b2 + sq) / (2.0 * a2);
|
|
262
|
+
}
|
|
263
|
+
if (!hit) discard;
|
|
264
|
+
|
|
265
|
+
float t = A0 + uz * s;
|
|
266
|
+
if (t < 0.0 || t > L) discard;
|
|
267
|
+
|
|
268
|
+
float depth_view = v_a.z + s;
|
|
269
|
+
|
|
270
|
+
float proj_t = t;
|
|
271
|
+
vec3 normal = vec3((ex - proj_t * u.x) / v_radius,
|
|
272
|
+
(ey - proj_t * u.y) / v_radius,
|
|
273
|
+
(s - proj_t * u.z) / v_radius);
|
|
274
|
+
|
|
275
|
+
float frac = t / L;
|
|
276
|
+
vec3 albedo = (frac < 0.5) ? v_color_a : v_color_b;
|
|
277
|
+
|
|
278
|
+
float nl = clamp(dot(normal, u_light_dir), 0.0, 1.0);
|
|
279
|
+
float nf = clamp(dot(normal, u_fill_dir), 0.0, 1.0) * u_fill_light;
|
|
280
|
+
float nh = clamp(dot(normal, u_half_vec), 0.0, 1.0);
|
|
281
|
+
float spec = pow(nh, u_shininess) * u_specular_strength;
|
|
282
|
+
float diff = u_ambient + nl + nf;
|
|
283
|
+
vec3 shaded = albedo * diff + vec3(spec);
|
|
284
|
+
|
|
285
|
+
if (u_depth_cue > 0.0) {
|
|
286
|
+
float f = clamp((depth_view - u_zmin) / u_zspan, 0.0, 1.0);
|
|
287
|
+
float fog = 1.0 - u_depth_cue * (1.0 - f);
|
|
288
|
+
shaded *= fog;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
frag_color = vec4(shaded, 1.0);
|
|
292
|
+
float nz = u_proj_z_scale * depth_view + u_proj_z_bias;
|
|
293
|
+
gl_FragDepth = nz * 0.5 + 0.5;
|
|
294
|
+
}
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
_CONE_VERT = """
|
|
298
|
+
#version 410 core
|
|
299
|
+
layout(location = 0) in vec2 in_corner;
|
|
300
|
+
layout(location = 1) in vec3 in_base;
|
|
301
|
+
layout(location = 2) in vec3 in_apex;
|
|
302
|
+
layout(location = 3) in float in_radius;
|
|
303
|
+
layout(location = 4) in vec3 in_color;
|
|
304
|
+
|
|
305
|
+
uniform mat4 u_proj;
|
|
306
|
+
|
|
307
|
+
out vec2 v_pixel;
|
|
308
|
+
out vec3 v_base;
|
|
309
|
+
out vec3 v_apex;
|
|
310
|
+
out float v_radius;
|
|
311
|
+
out vec3 v_color;
|
|
312
|
+
|
|
313
|
+
void main() {
|
|
314
|
+
// Only the base has nonzero radius (the apex tapers to a point), but
|
|
315
|
+
// padding the bbox by in_radius at both corners is still a safe
|
|
316
|
+
// (slightly generous near the apex) bound -- same approach _BOND_VERT
|
|
317
|
+
// uses for a cylinder's uniform radius.
|
|
318
|
+
vec2 lo = min(in_base.xy, in_apex.xy) - vec2(in_radius);
|
|
319
|
+
vec2 hi = max(in_base.xy, in_apex.xy) + vec2(in_radius);
|
|
320
|
+
vec2 center = 0.5 * (lo + hi);
|
|
321
|
+
vec2 half_extent = max(0.5 * (hi - lo), vec2(1e-4));
|
|
322
|
+
vec2 pos = center + in_corner * half_extent;
|
|
323
|
+
|
|
324
|
+
v_pixel = pos;
|
|
325
|
+
v_base = in_base;
|
|
326
|
+
v_apex = in_apex;
|
|
327
|
+
v_radius = in_radius;
|
|
328
|
+
v_color = in_color;
|
|
329
|
+
|
|
330
|
+
float z_mid = 0.5 * (in_base.z + in_apex.z);
|
|
331
|
+
gl_Position = u_proj * vec4(pos, z_mid, 1.0);
|
|
332
|
+
}
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
_CONE_FRAG = """
|
|
336
|
+
#version 410 core
|
|
337
|
+
in vec2 v_pixel;
|
|
338
|
+
in vec3 v_base;
|
|
339
|
+
in vec3 v_apex;
|
|
340
|
+
in float v_radius;
|
|
341
|
+
in vec3 v_color;
|
|
342
|
+
|
|
343
|
+
uniform float u_proj_z_scale;
|
|
344
|
+
uniform float u_proj_z_bias;
|
|
345
|
+
uniform vec3 u_light_dir;
|
|
346
|
+
uniform vec3 u_half_vec;
|
|
347
|
+
uniform vec3 u_fill_dir;
|
|
348
|
+
uniform float u_ambient;
|
|
349
|
+
uniform float u_fill_light;
|
|
350
|
+
uniform float u_specular_strength;
|
|
351
|
+
uniform float u_shininess;
|
|
352
|
+
uniform float u_depth_cue;
|
|
353
|
+
uniform float u_zmin;
|
|
354
|
+
uniform float u_zspan;
|
|
355
|
+
|
|
356
|
+
out vec4 frag_color;
|
|
357
|
+
|
|
358
|
+
// GLSL port of render.Renderer._draw_cone_segment's math -- see that
|
|
359
|
+
// docstring for the derivation. Target radius at axial offset s is
|
|
360
|
+
// C + D*s (linear from R at the base to 0 at the apex); this folds an
|
|
361
|
+
// extra (C+D*s)^2 term into the cylinder's usual quadratic, so unlike the
|
|
362
|
+
// cylinder shader the leading coefficient a2p isn't sign-guaranteed and
|
|
363
|
+
// both roots must be checked, picking the nearer-camera (larger s) one
|
|
364
|
+
// that lies on the finite cone (0<=t<=L) with non-negative radius
|
|
365
|
+
// (rejecting the mirror nappe beyond the apex that squaring introduces).
|
|
366
|
+
void main() {
|
|
367
|
+
vec3 axis = v_apex - v_base;
|
|
368
|
+
float L = length(axis);
|
|
369
|
+
if (L < 1e-6 || v_radius <= 0.0) discard;
|
|
370
|
+
vec3 u = axis / L;
|
|
371
|
+
float R = v_radius;
|
|
372
|
+
|
|
373
|
+
float ex = v_pixel.x - v_base.x;
|
|
374
|
+
float ey = v_pixel.y - v_base.y;
|
|
375
|
+
float uz = u.z;
|
|
376
|
+
float A0 = ex * u.x + ey * u.y;
|
|
377
|
+
float a2 = 1.0 - uz * uz;
|
|
378
|
+
float b2 = -2.0 * A0 * uz;
|
|
379
|
+
float c2 = ex * ex + ey * ey - A0 * A0;
|
|
380
|
+
|
|
381
|
+
float k = R / L;
|
|
382
|
+
float C = R - k * A0;
|
|
383
|
+
float D = -k * uz;
|
|
384
|
+
float a2p = a2 - D * D;
|
|
385
|
+
float b2p = b2 - 2.0 * C * D;
|
|
386
|
+
float c2p = c2 - C * C;
|
|
387
|
+
|
|
388
|
+
bool near_parallel = abs(a2p) < 1e-9;
|
|
389
|
+
float safe_a2p = near_parallel ? 1.0 : a2p;
|
|
390
|
+
float disc = b2p * b2p - 4.0 * a2p * c2p;
|
|
391
|
+
bool has_root = disc >= 0.0;
|
|
392
|
+
float sq = sqrt(max(disc, 0.0));
|
|
393
|
+
float s_plus = (-b2p + sq) / (2.0 * safe_a2p);
|
|
394
|
+
float s_minus = (-b2p - sq) / (2.0 * safe_a2p);
|
|
395
|
+
|
|
396
|
+
bool b2p_nonzero = abs(b2p) >= 1e-12;
|
|
397
|
+
float safe_b2p = b2p_nonzero ? b2p : 1.0;
|
|
398
|
+
float s_lin = -c2p / safe_b2p;
|
|
399
|
+
bool lin_valid = near_parallel && b2p_nonzero;
|
|
400
|
+
|
|
401
|
+
float t_plus = A0 + uz * s_plus;
|
|
402
|
+
float r_plus = C + D * s_plus;
|
|
403
|
+
bool v_plus = has_root && !near_parallel && (t_plus >= 0.0) && (t_plus <= L) && (r_plus >= -1e-6);
|
|
404
|
+
|
|
405
|
+
float t_minus = A0 + uz * s_minus;
|
|
406
|
+
float r_minus = C + D * s_minus;
|
|
407
|
+
bool v_minus = has_root && !near_parallel && (t_minus >= 0.0) && (t_minus <= L) && (r_minus >= -1e-6);
|
|
408
|
+
|
|
409
|
+
float t_lin = A0 + uz * s_lin;
|
|
410
|
+
float r_lin = C + D * s_lin;
|
|
411
|
+
bool v_lin = lin_valid && (t_lin >= 0.0) && (t_lin <= L) && (r_lin >= -1e-6);
|
|
412
|
+
|
|
413
|
+
const float NEG_INF = -1e30;
|
|
414
|
+
float s = NEG_INF;
|
|
415
|
+
if (v_plus) s = max(s, s_plus);
|
|
416
|
+
if (v_minus) s = max(s, s_minus);
|
|
417
|
+
if (v_lin) s = max(s, s_lin);
|
|
418
|
+
if (s <= NEG_INF * 0.5) discard;
|
|
419
|
+
|
|
420
|
+
float t = A0 + uz * s;
|
|
421
|
+
float depth_view = v_base.z + s;
|
|
422
|
+
|
|
423
|
+
float wx = ex - t * u.x;
|
|
424
|
+
float wy = ey - t * u.y;
|
|
425
|
+
float wz = s - t * u.z;
|
|
426
|
+
float r_t = max(C + D * s, 1e-6);
|
|
427
|
+
vec3 normal = normalize(vec3(wx / r_t, wy / r_t, wz / r_t) * L + u * R);
|
|
428
|
+
|
|
429
|
+
float nl = clamp(dot(normal, u_light_dir), 0.0, 1.0);
|
|
430
|
+
float nf = clamp(dot(normal, u_fill_dir), 0.0, 1.0) * u_fill_light;
|
|
431
|
+
float nh = clamp(dot(normal, u_half_vec), 0.0, 1.0);
|
|
432
|
+
float spec = pow(nh, u_shininess) * u_specular_strength;
|
|
433
|
+
float diff = u_ambient + nl + nf;
|
|
434
|
+
vec3 shaded = v_color * diff + vec3(spec);
|
|
435
|
+
|
|
436
|
+
if (u_depth_cue > 0.0) {
|
|
437
|
+
float f = clamp((depth_view - u_zmin) / u_zspan, 0.0, 1.0);
|
|
438
|
+
float fog = 1.0 - u_depth_cue * (1.0 - f);
|
|
439
|
+
shaded *= fog;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
frag_color = vec4(shaded, 1.0);
|
|
443
|
+
float nz = u_proj_z_scale * depth_view + u_proj_z_bias;
|
|
444
|
+
gl_FragDepth = nz * 0.5 + 0.5;
|
|
445
|
+
}
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
_QUAD_CORNERS = np.array([-1, -1, 1, -1, -1, 1, 1, 1], dtype=np.float32)
|
|
449
|
+
|
|
450
|
+
# Fullscreen-quad box-average downsample: for each output pixel, average the
|
|
451
|
+
# ds*ds block of source texels. Exact box filter (matches the CPU _downsample),
|
|
452
|
+
# and it runs on the GPU in ~1ms instead of ~200ms in numpy.
|
|
453
|
+
_DOWNSAMPLE_VERT = """
|
|
454
|
+
#version 410 core
|
|
455
|
+
layout(location = 0) in vec2 in_corner;
|
|
456
|
+
void main() { gl_Position = vec4(in_corner, 0.0, 1.0); }
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
_DOWNSAMPLE_FRAG = """
|
|
460
|
+
#version 410 core
|
|
461
|
+
uniform sampler2D u_src;
|
|
462
|
+
uniform int u_factor;
|
|
463
|
+
out vec4 frag_color;
|
|
464
|
+
void main() {
|
|
465
|
+
ivec2 base = ivec2(gl_FragCoord.xy) * u_factor;
|
|
466
|
+
vec4 acc = vec4(0.0);
|
|
467
|
+
for (int dy = 0; dy < u_factor; ++dy)
|
|
468
|
+
for (int dx = 0; dx < u_factor; ++dx)
|
|
469
|
+
acc += texelFetch(u_src, base + ivec2(dx, dy), 0);
|
|
470
|
+
acc /= float(u_factor * u_factor);
|
|
471
|
+
// The source averaged in premultiplied space (undrawn texels are 0,0,0,0),
|
|
472
|
+
// so divide back out to straight alpha -- what the Kitty/PNG encoders want.
|
|
473
|
+
// Opaque scenes have alpha==1 everywhere, making this a no-op there.
|
|
474
|
+
if (acc.a > 0.0) acc.rgb /= acc.a;
|
|
475
|
+
frag_color = acc;
|
|
476
|
+
}
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _create_standalone_context() -> moderngl.Context:
|
|
481
|
+
"""Create a standalone context, preferring EGL over moderngl's default.
|
|
482
|
+
|
|
483
|
+
On Linux, ``moderngl.create_context(standalone=True)`` always picks the
|
|
484
|
+
GLX/X11 backend (see ``glcontext.default_backend``) regardless of
|
|
485
|
+
whether ``DISPLAY`` actually has working GL behind it -- an SSH session
|
|
486
|
+
with X11 forwarding sets ``DISPLAY`` to a forwarded/virtual X server
|
|
487
|
+
that has no real (or even software) GLX support, so GLX context
|
|
488
|
+
creation fails with a real GPU sitting right there. EGL's device
|
|
489
|
+
platform talks to the GPU directly via ``/dev/dri/renderD*``, bypassing
|
|
490
|
+
X entirely, so try it first and fall back to moderngl's default (GLX on
|
|
491
|
+
Linux, CGL on macOS) if EGL isn't available.
|
|
492
|
+
"""
|
|
493
|
+
try:
|
|
494
|
+
return moderngl.create_context(standalone=True, backend="egl")
|
|
495
|
+
except Exception:
|
|
496
|
+
return moderngl.create_context(standalone=True)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class GLRenderer:
|
|
500
|
+
"""GPU sphere/cylinder impostor renderer.
|
|
501
|
+
|
|
502
|
+
Mirrors ``render.Renderer``'s ``__init__(width, height)``/
|
|
503
|
+
``resize(width, height)`` shape so callers can construct/resize either
|
|
504
|
+
renderer identically, but ``render()`` takes generic primitive batches
|
|
505
|
+
plus a raw projection matrix rather than a molecule-specific object.
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
def __init__(self, width: int, height: int):
|
|
509
|
+
self.ctx = _create_standalone_context()
|
|
510
|
+
self.width = int(width)
|
|
511
|
+
self.height = int(height)
|
|
512
|
+
self._quad_vbo = self.ctx.buffer(_QUAD_CORNERS.tobytes())
|
|
513
|
+
self._atom_program = self.ctx.program(vertex_shader=_ATOM_VERT, fragment_shader=_ATOM_FRAG)
|
|
514
|
+
self._bond_program = self.ctx.program(vertex_shader=_BOND_VERT, fragment_shader=_BOND_FRAG)
|
|
515
|
+
self._cone_program = self.ctx.program(vertex_shader=_CONE_VERT, fragment_shader=_CONE_FRAG)
|
|
516
|
+
self._downsample_program = self.ctx.program(
|
|
517
|
+
vertex_shader=_DOWNSAMPLE_VERT, fragment_shader=_DOWNSAMPLE_FRAG)
|
|
518
|
+
self._downsample_vao = self.ctx.vertex_array(
|
|
519
|
+
self._downsample_program, [(self._quad_vbo, "2f", "in_corner")])
|
|
520
|
+
self._fbo = None
|
|
521
|
+
self._color_tex = None # color attachment is a texture so the
|
|
522
|
+
self._depth_rb = None # downsample pass can sample it
|
|
523
|
+
self._resolve_fbo = None # smaller FBO the downsample pass writes to
|
|
524
|
+
self._resolve_size = None
|
|
525
|
+
self._build_fbo(self.width, self.height)
|
|
526
|
+
|
|
527
|
+
def _build_fbo(self, w: int, h: int) -> None:
|
|
528
|
+
if self._fbo is not None:
|
|
529
|
+
self._fbo.release()
|
|
530
|
+
self._color_tex.release()
|
|
531
|
+
self._depth_rb.release()
|
|
532
|
+
self._color_tex = self.ctx.texture((w, h), components=4, dtype="f1")
|
|
533
|
+
self._depth_rb = self.ctx.depth_renderbuffer((w, h))
|
|
534
|
+
self._fbo = self.ctx.framebuffer(color_attachments=[self._color_tex], depth_attachment=self._depth_rb)
|
|
535
|
+
|
|
536
|
+
def _resolve_target(self, w: int, h: int):
|
|
537
|
+
"""A cached (rebuilt on size change) color-only FBO at (w, h) that the
|
|
538
|
+
box-average downsample pass renders into."""
|
|
539
|
+
if self._resolve_size != (w, h):
|
|
540
|
+
if self._resolve_fbo is not None:
|
|
541
|
+
self._resolve_fbo.release()
|
|
542
|
+
self._resolve_rb.release()
|
|
543
|
+
self._resolve_rb = self.ctx.renderbuffer((w, h), components=4, dtype="f1")
|
|
544
|
+
self._resolve_fbo = self.ctx.framebuffer(color_attachments=[self._resolve_rb])
|
|
545
|
+
self._resolve_size = (w, h)
|
|
546
|
+
return self._resolve_fbo
|
|
547
|
+
|
|
548
|
+
def resize(self, width: int, height: int) -> None:
|
|
549
|
+
width, height = int(width), int(height)
|
|
550
|
+
if (width, height) == (self.width, self.height):
|
|
551
|
+
return
|
|
552
|
+
self.width, self.height = width, height
|
|
553
|
+
self._build_fbo(width, height)
|
|
554
|
+
|
|
555
|
+
# ------------------------------------------------------------------
|
|
556
|
+
def render(self, spheres: SphereBatch, cylinders: CylinderBatch,
|
|
557
|
+
proj: np.ndarray, shading: ShadingParams | None = None,
|
|
558
|
+
downsample: int = 1, cones: ConeBatch | None = None) -> np.ndarray:
|
|
559
|
+
"""Render *spheres*, *cylinders* and *cones* under projection *proj*.
|
|
560
|
+
|
|
561
|
+
*cones* defaults to empty so existing call sites that only know about
|
|
562
|
+
spheres/cylinders keep working unchanged.
|
|
563
|
+
|
|
564
|
+
*proj* is a 4x4 matrix in the usual mathematical convention
|
|
565
|
+
(``[nx,ny,nz,1] = proj @ [vx,vy,vz,1]``, row-major indexing) mapping
|
|
566
|
+
the primitives' coordinate space to clip-space NDC. Its ``[2,2]``/
|
|
567
|
+
``[2,3]`` entries double as the scale/bias used to reproject each
|
|
568
|
+
fragment's own analytic depth (see ``gl_adapter._build_projection``
|
|
569
|
+
for how vimol builds one from its orthographic ``Camera``).
|
|
570
|
+
|
|
571
|
+
*downsample* (>=1) supersamples: the scene is drawn at the full
|
|
572
|
+
``(width, height)`` and box-averaged down by this factor **on the GPU**
|
|
573
|
+
(a fullscreen downsample-shader pass, ~1ms) rather than in numpy on the
|
|
574
|
+
CPU (~200ms at full screen). The returned image is therefore
|
|
575
|
+
``(height//downsample, width//downsample)``. The caller sizes this
|
|
576
|
+
renderer to display-size*factor and passes the same factor here.
|
|
577
|
+
"""
|
|
578
|
+
shading = shading or ShadingParams()
|
|
579
|
+
cones = cones or ConeBatch.empty()
|
|
580
|
+
W, H = self.width, self.height
|
|
581
|
+
downsample = max(1, int(downsample))
|
|
582
|
+
|
|
583
|
+
proj = np.asarray(proj, dtype=np.float64)
|
|
584
|
+
z_scale = float(proj[2, 2])
|
|
585
|
+
z_bias = float(proj[2, 3])
|
|
586
|
+
proj_gl = np.ascontiguousarray(proj.T, dtype=np.float32) # column-major for GLSL
|
|
587
|
+
|
|
588
|
+
centers = np.asarray(spheres.centers, dtype=np.float32).reshape(-1, 3)
|
|
589
|
+
radii_s = np.asarray(spheres.radii, dtype=np.float32).reshape(-1)
|
|
590
|
+
colors_s = np.asarray(spheres.colors, dtype=np.float32).reshape(-1, 3)
|
|
591
|
+
keep_s = radii_s > 0
|
|
592
|
+
centers, radii_s, colors_s = centers[keep_s], radii_s[keep_s], colors_s[keep_s]
|
|
593
|
+
|
|
594
|
+
ca = np.asarray(cylinders.a, dtype=np.float32).reshape(-1, 3)
|
|
595
|
+
cb = np.asarray(cylinders.b, dtype=np.float32).reshape(-1, 3)
|
|
596
|
+
radii_c = np.asarray(cylinders.radii, dtype=np.float32).reshape(-1)
|
|
597
|
+
colors_a = np.asarray(cylinders.colors_a, dtype=np.float32).reshape(-1, 3)
|
|
598
|
+
colors_b = np.asarray(cylinders.colors_b, dtype=np.float32).reshape(-1, 3)
|
|
599
|
+
keep_c = radii_c > 0
|
|
600
|
+
ca, cb, radii_c, colors_a, colors_b = (
|
|
601
|
+
ca[keep_c], cb[keep_c], radii_c[keep_c], colors_a[keep_c], colors_b[keep_c],
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
cone_base = np.asarray(cones.base, dtype=np.float32).reshape(-1, 3)
|
|
605
|
+
cone_apex = np.asarray(cones.apex, dtype=np.float32).reshape(-1, 3)
|
|
606
|
+
radii_h = np.asarray(cones.radius, dtype=np.float32).reshape(-1)
|
|
607
|
+
colors_h = np.asarray(cones.color, dtype=np.float32).reshape(-1, 3)
|
|
608
|
+
keep_h = radii_h > 0
|
|
609
|
+
cone_base, cone_apex, radii_h, colors_h = (
|
|
610
|
+
cone_base[keep_h], cone_apex[keep_h], radii_h[keep_h], colors_h[keep_h],
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if len(centers) or len(ca) or len(cone_base):
|
|
614
|
+
depths = np.concatenate([centers[:, 2], ca[:, 2], cb[:, 2], cone_base[:, 2], cone_apex[:, 2]])
|
|
615
|
+
zmin = float(depths.min())
|
|
616
|
+
zspan = max(float(depths.max()) - zmin, 1e-6)
|
|
617
|
+
else:
|
|
618
|
+
zmin, zspan = 0.0, 1.0
|
|
619
|
+
|
|
620
|
+
light = _normalize(np.asarray(shading.light_dir, dtype=np.float64))
|
|
621
|
+
halfv = _normalize(light + np.array([0.0, 0.0, 1.0]))
|
|
622
|
+
fill = _normalize(np.array([-light[0], -light[1], 0.6]))
|
|
623
|
+
|
|
624
|
+
self._fbo.use()
|
|
625
|
+
self.ctx.viewport = (0, 0, W, H)
|
|
626
|
+
if shading.transparent:
|
|
627
|
+
self._fbo.clear(0.0, 0.0, 0.0, 0.0, depth=1.0)
|
|
628
|
+
else:
|
|
629
|
+
bg = shading.background
|
|
630
|
+
self._fbo.clear(bg[0], bg[1], bg[2], 1.0, depth=1.0)
|
|
631
|
+
self.ctx.enable(moderngl.DEPTH_TEST)
|
|
632
|
+
self.ctx.disable(moderngl.BLEND)
|
|
633
|
+
self.ctx.depth_func = "<"
|
|
634
|
+
|
|
635
|
+
common = dict(
|
|
636
|
+
u_proj_z_scale=z_scale, u_proj_z_bias=z_bias,
|
|
637
|
+
u_light_dir=tuple(light), u_half_vec=tuple(halfv), u_fill_dir=tuple(fill),
|
|
638
|
+
u_ambient=shading.ambient, u_fill_light=shading.fill_light,
|
|
639
|
+
u_specular_strength=shading.specular_strength, u_shininess=shading.shininess,
|
|
640
|
+
u_depth_cue=shading.depth_cue, u_zmin=zmin, u_zspan=zspan,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if len(ca):
|
|
644
|
+
self._draw_bonds(ca, cb, radii_c, colors_a, colors_b, proj_gl, common)
|
|
645
|
+
if len(centers):
|
|
646
|
+
self._draw_atoms(centers, radii_s, colors_s, proj_gl, common)
|
|
647
|
+
if len(cone_base):
|
|
648
|
+
self._draw_cones(cone_base, cone_apex, radii_h, colors_h, proj_gl, common)
|
|
649
|
+
|
|
650
|
+
if downsample > 1:
|
|
651
|
+
tw, th = W // downsample, H // downsample
|
|
652
|
+
resolve = self._resolve_target(tw, th)
|
|
653
|
+
resolve.use()
|
|
654
|
+
self.ctx.viewport = (0, 0, tw, th)
|
|
655
|
+
self.ctx.disable(moderngl.DEPTH_TEST)
|
|
656
|
+
self._color_tex.use(location=0)
|
|
657
|
+
self._downsample_program["u_src"] = 0
|
|
658
|
+
self._downsample_program["u_factor"] = downsample
|
|
659
|
+
self._downsample_vao.render(moderngl.TRIANGLE_STRIP)
|
|
660
|
+
raw = resolve.read(components=4, dtype="f1")
|
|
661
|
+
arr = np.frombuffer(raw, dtype=np.uint8).reshape(th, tw, 4)
|
|
662
|
+
else:
|
|
663
|
+
raw = self._fbo.read(components=4, dtype="f1")
|
|
664
|
+
arr = np.frombuffer(raw, dtype=np.uint8).reshape(H, W, 4)
|
|
665
|
+
arr = np.flipud(arr)
|
|
666
|
+
|
|
667
|
+
# Edge pixels come back straight-alpha already (the downsample shader
|
|
668
|
+
# un-premultiplies; the ds==1 path never premultiplies).
|
|
669
|
+
if not shading.transparent:
|
|
670
|
+
return np.ascontiguousarray(arr[..., :3])
|
|
671
|
+
return np.ascontiguousarray(arr)
|
|
672
|
+
|
|
673
|
+
# ------------------------------------------------------------------
|
|
674
|
+
def _set_uniforms(self, program, proj_gl: np.ndarray, common: dict) -> None:
|
|
675
|
+
program["u_proj"].write(proj_gl.tobytes())
|
|
676
|
+
for name, value in common.items():
|
|
677
|
+
program[name].value = value
|
|
678
|
+
|
|
679
|
+
def _draw_atoms(self, centers, radii, colors, proj_gl, common) -> None:
|
|
680
|
+
n = centers.shape[0]
|
|
681
|
+
data = np.empty((n, 7), dtype=np.float32)
|
|
682
|
+
data[:, 0:3] = centers
|
|
683
|
+
data[:, 3] = radii
|
|
684
|
+
data[:, 4:7] = colors
|
|
685
|
+
inst_vbo = self.ctx.buffer(data.tobytes())
|
|
686
|
+
vao = self.ctx.vertex_array(
|
|
687
|
+
self._atom_program,
|
|
688
|
+
[
|
|
689
|
+
(self._quad_vbo, "2f", "in_corner"),
|
|
690
|
+
(inst_vbo, "3f 1f 3f/i", "in_center", "in_radius", "in_color"),
|
|
691
|
+
],
|
|
692
|
+
)
|
|
693
|
+
self._set_uniforms(self._atom_program, proj_gl, common)
|
|
694
|
+
vao.render(moderngl.TRIANGLE_STRIP, instances=n)
|
|
695
|
+
vao.release()
|
|
696
|
+
inst_vbo.release()
|
|
697
|
+
|
|
698
|
+
def _draw_bonds(self, a, b, radii, colors_a, colors_b, proj_gl, common) -> None:
|
|
699
|
+
n = a.shape[0]
|
|
700
|
+
data = np.empty((n, 13), dtype=np.float32)
|
|
701
|
+
data[:, 0:3] = a
|
|
702
|
+
data[:, 3:6] = b
|
|
703
|
+
data[:, 6] = radii
|
|
704
|
+
data[:, 7:10] = colors_a
|
|
705
|
+
data[:, 10:13] = colors_b
|
|
706
|
+
inst_vbo = self.ctx.buffer(data.tobytes())
|
|
707
|
+
vao = self.ctx.vertex_array(
|
|
708
|
+
self._bond_program,
|
|
709
|
+
[
|
|
710
|
+
(self._quad_vbo, "2f", "in_corner"),
|
|
711
|
+
(inst_vbo, "3f 3f 1f 3f 3f/i", "in_a", "in_b", "in_radius", "in_color_a", "in_color_b"),
|
|
712
|
+
],
|
|
713
|
+
)
|
|
714
|
+
self._set_uniforms(self._bond_program, proj_gl, common)
|
|
715
|
+
vao.render(moderngl.TRIANGLE_STRIP, instances=n)
|
|
716
|
+
vao.release()
|
|
717
|
+
inst_vbo.release()
|
|
718
|
+
|
|
719
|
+
def _draw_cones(self, base, apex, radii, colors, proj_gl, common) -> None:
|
|
720
|
+
n = base.shape[0]
|
|
721
|
+
data = np.empty((n, 10), dtype=np.float32)
|
|
722
|
+
data[:, 0:3] = base
|
|
723
|
+
data[:, 3:6] = apex
|
|
724
|
+
data[:, 6] = radii
|
|
725
|
+
data[:, 7:10] = colors
|
|
726
|
+
inst_vbo = self.ctx.buffer(data.tobytes())
|
|
727
|
+
vao = self.ctx.vertex_array(
|
|
728
|
+
self._cone_program,
|
|
729
|
+
[
|
|
730
|
+
(self._quad_vbo, "2f", "in_corner"),
|
|
731
|
+
(inst_vbo, "3f 3f 1f 3f/i", "in_base", "in_apex", "in_radius", "in_color"),
|
|
732
|
+
],
|
|
733
|
+
)
|
|
734
|
+
self._set_uniforms(self._cone_program, proj_gl, common)
|
|
735
|
+
vao.render(moderngl.TRIANGLE_STRIP, instances=n)
|
|
736
|
+
vao.release()
|
|
737
|
+
inst_vbo.release()
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _normalize(v: np.ndarray) -> np.ndarray:
|
|
741
|
+
n = np.linalg.norm(v)
|
|
742
|
+
return v / n if n > 1e-12 else v
|