mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
733 lines
30 KiB
Python
733 lines
30 KiB
Python
"""GPU motion-graphics engine for the MobileGL showreel (moderngl, OpenGL 4.3+ core).
|
|
|
|
Everything is laid out in logical 1920x1080 pixels, y down. A frame is the average of `samples`
|
|
sub-frames across a shutter (motion blur), then bloom / aberration / grain / vignette in post.
|
|
Set MGL_LIBGL to an opengl32.dll to render through another GL implementation (MobileGL)."""
|
|
import math
|
|
import os
|
|
import functools
|
|
from collections import OrderedDict
|
|
|
|
import numpy as np
|
|
import moderngl
|
|
import freetype
|
|
import uharfbuzz as hb
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
LW, LH = 1920, 1080
|
|
|
|
|
|
# --- colour ----------------------------------------------------------------------------------------
|
|
def srgb2lin(c):
|
|
c = c / 255.0
|
|
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
|
|
|
|
def hexc(h, k=1.0, a=1.0):
|
|
"""'#rrggbb' -> linear (r, g, b, a), rgb scaled by k (k > 1 for emissive glow)."""
|
|
h = h.lstrip('#')
|
|
return (srgb2lin(int(h[0:2], 16)) * k, srgb2lin(int(h[2:4], 16)) * k, srgb2lin(int(h[4:6], 16)) * k, a)
|
|
|
|
|
|
def mix(a, b, u):
|
|
return tuple(x + (y - x) * u for x, y in zip(a, b))
|
|
|
|
|
|
def with_alpha(c, a):
|
|
return (c[0], c[1], c[2], c[3] * a)
|
|
|
|
|
|
def scale_rgb(c, k):
|
|
return (c[0] * k, c[1] * k, c[2] * k, c[3])
|
|
|
|
|
|
# --- fonts -----------------------------------------------------------------------------------------
|
|
class Face:
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.hbface = hb.Face(hb.Blob.from_file_path(path))
|
|
self.hbfont = hb.Font(self.hbface)
|
|
self.upem = self.hbface.upem
|
|
self.ft = freetype.Face(path)
|
|
self.axes = [a.tag for a in self.ft.get_variation_info().axes] if self.ft.has_multiple_masters else []
|
|
self.ft.set_char_size(1000 * 64)
|
|
self.ft.load_char('H', freetype.FT_LOAD_NO_HINTING)
|
|
self.cap = self.ft.glyph.metrics.horiBearingY / 64 / 1000 # cap height / em
|
|
self.ft.load_char('x', freetype.FT_LOAD_NO_HINTING)
|
|
self.xh = self.ft.glyph.metrics.horiBearingY / 64 / 1000
|
|
|
|
def _vary(self, var):
|
|
coords = tuple(var.get(a, None) for a in self.axes)
|
|
if self.axes:
|
|
info = {a.tag: a for a in self.ft.get_variation_info().axes}
|
|
full = tuple(v if v is not None else info[a].default for a, v in zip(self.axes, coords))
|
|
self.ft.set_var_design_coords(full)
|
|
self.hbfont.set_variations({a: v for a, v in zip(self.axes, full)})
|
|
|
|
def shape(self, s, size, var, tracking=0.0):
|
|
"""-> list of (gid, x, y, cluster) pen positions in px, and total advance."""
|
|
self._vary(var)
|
|
buf = hb.Buffer()
|
|
buf.add_str(s)
|
|
buf.guess_segment_properties()
|
|
hb.shape(self.hbfont, buf, {'kern': True, 'liga': True})
|
|
k = size / self.upem
|
|
out = []
|
|
x = 0.0
|
|
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
|
out.append((info.codepoint, x + pos.x_offset * k, -pos.y_offset * k, info.cluster))
|
|
x += pos.x_advance * k + tracking
|
|
return out, x - (tracking if out else 0)
|
|
|
|
def raster(self, gid, size, var):
|
|
self._vary(var)
|
|
self.ft.set_char_size(int(round(size * 64)))
|
|
self.ft.load_glyph(gid, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_NO_HINTING)
|
|
g = self.ft.glyph
|
|
b = g.bitmap
|
|
arr = np.array(b.buffer, dtype=np.uint8).reshape(b.rows, b.width) if b.rows else np.zeros((0, 0), np.uint8)
|
|
return arr, g.bitmap_left, g.bitmap_top
|
|
|
|
|
|
FONT_FILES = {
|
|
'archivo': 'Archivo[wdth,wght].ttf',
|
|
'archivo-i': 'Archivo-Italic[wdth,wght].ttf',
|
|
'mono': 'JetBrainsMono[wght].ttf',
|
|
}
|
|
|
|
|
|
@functools.lru_cache(None)
|
|
def face(name):
|
|
return Face(os.path.join(HERE, 'fonts', FONT_FILES[name]))
|
|
|
|
|
|
SIZE_LADDER = 2 ** (1 / 10)
|
|
|
|
|
|
def quant_size(px):
|
|
"""Raster size ladder (~7% steps) so animated sizes reuse textures."""
|
|
k = math.ceil(math.log(max(px, 2)) / math.log(SIZE_LADDER) - 1e-6)
|
|
return SIZE_LADDER ** k
|
|
|
|
|
|
# --- shaders ---------------------------------------------------------------------------------------
|
|
def _read(name):
|
|
with open(os.path.join(HERE, 'shaders', name), encoding='utf-8') as f:
|
|
return f.read()
|
|
|
|
|
|
def preprocess(src):
|
|
out = []
|
|
for line in src.splitlines():
|
|
if line.strip().startswith('#include'):
|
|
name = line.split('"')[1]
|
|
out.append(preprocess(COMMON if name == 'common.glsl' else _read(name)))
|
|
else:
|
|
out.append(line)
|
|
return '\n'.join(out)
|
|
|
|
|
|
VS_FULL = '''#version 430
|
|
out vec2 v_uv;
|
|
void main(){ vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); v_uv = p; gl_Position = vec4(p * 2.0 - 1.0, 0, 1); }
|
|
'''
|
|
|
|
SHAPE_VS = '''#version 430
|
|
#include "common.glsl"
|
|
in vec2 a_corner;
|
|
in vec4 i_rect; in vec4 i_xf; in vec4 i_kind; in vec4 i_p0; in vec4 i_p1; in vec4 i_p2; in vec4 i_p3;
|
|
in vec4 i_fill; in vec4 i_stroke; in vec4 i_sw;
|
|
out vec2 v_lp;
|
|
flat out vec4 v_kind; flat out vec4 v_p0; flat out vec4 v_p1; flat out vec4 v_p2; flat out vec4 v_p3;
|
|
flat out vec4 v_fill; flat out vec4 v_stroke; flat out vec4 v_sw;
|
|
void main(){
|
|
float pad = i_sw.x + 4.0 * i_sw.y + 2.0;
|
|
vec2 lp = mix(i_rect.xy - pad, i_rect.zw + pad, a_corner);
|
|
float c = cos(i_xf.z), s = sin(i_xf.z);
|
|
vec2 wp = i_xf.xy + i_xf.w * vec2(c * lp.x - s * lp.y, s * lp.x + c * lp.y);
|
|
gl_Position = vec4(to_ndc(cam(wp)), 0, 1);
|
|
v_lp = lp; v_kind = i_kind; v_p0 = i_p0; v_p1 = i_p1; v_p2 = i_p2; v_p3 = i_p3;
|
|
v_fill = i_fill; v_stroke = i_stroke; v_sw = i_sw;
|
|
}
|
|
'''
|
|
|
|
SHAPE_FS = '''#version 430
|
|
#include "common.glsl"
|
|
in vec2 v_lp;
|
|
flat in vec4 v_kind; flat in vec4 v_p0; flat in vec4 v_p1; flat in vec4 v_p2; flat in vec4 v_p3;
|
|
flat in vec4 v_fill; flat in vec4 v_stroke; flat in vec4 v_sw;
|
|
out vec4 o;
|
|
vec2 vtx(int i){
|
|
vec4 q = i < 2 ? v_p0 : i < 4 ? v_p1 : i < 6 ? v_p2 : v_p3;
|
|
return (i % 2 == 0) ? q.xy : q.zw;
|
|
}
|
|
float sd_poly(vec2 p, int n){
|
|
vec2 v0 = vtx(0);
|
|
float d = dot(p - v0, p - v0), sg = 1.0;
|
|
for (int i = 0, j = n - 1; i < n; j = i, i++){
|
|
vec2 vi = vtx(i), vj = vtx(j);
|
|
vec2 e = vj - vi, w = p - vi;
|
|
vec2 b = w - e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
|
|
d = min(d, dot(b, b));
|
|
bvec3 cnd = bvec3(p.y >= vi.y, p.y < vj.y, e.x * w.y > e.y * w.x);
|
|
if (all(cnd) || all(not(cnd))) sg *= -1.0;
|
|
}
|
|
return sg * sqrt(d);
|
|
}
|
|
float sd_arc(vec2 p, float r, float th, float a0, float a1){
|
|
float a = atan(p.y, p.x);
|
|
float mid = 0.5 * (a0 + a1), half_ = 0.5 * (a1 - a0);
|
|
float da = mod(a - mid + PI, 2.0 * PI) - PI;
|
|
if (abs(da) <= half_) return abs(length(p) - r) - th * 0.5;
|
|
vec2 e0 = r * vec2(cos(a0), sin(a0)), e1 = r * vec2(cos(a1), sin(a1));
|
|
return min(length(p - e0), length(p - e1)) - th * 0.5;
|
|
}
|
|
void main(){
|
|
int kind = int(v_kind.x + 0.5);
|
|
vec2 p = v_lp;
|
|
float d;
|
|
if (kind == 0) d = length(p) - v_p0.x;
|
|
else if (kind == 1) { vec2 q = abs(p) - v_p0.xy + v_p0.z; d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - v_p0.z; }
|
|
else if (kind == 2) { vec2 a = v_p0.xy, b = v_p0.zw; vec2 pa = p - a, ba = b - a;
|
|
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
|
|
d = length(pa - ba * h) - mix(v_p1.x, v_p1.y, h); }
|
|
else if (kind == 5) d = sd_arc(p, v_p0.x, v_p0.y, v_p0.z, v_p0.w);
|
|
else d = sd_poly(p, int(v_kind.y + 0.5)) - v_kind.z;
|
|
float aa = max(length(fwidth(p)) * 0.75, 1e-4);
|
|
vec4 col = vec4(0);
|
|
float af = clamp(0.5 - d / aa, 0.0, 1.0) * v_fill.a;
|
|
col = vec4(v_fill.rgb * af, af);
|
|
if (v_sw.x > 0.0) {
|
|
float as_ = clamp(0.5 - (abs(d) - v_sw.x * 0.5) / aa, 0.0, 1.0) * v_stroke.a;
|
|
col = vec4(v_stroke.rgb * as_, as_) + col * (1.0 - as_);
|
|
}
|
|
if (v_sw.y > 0.0) { // emissive halo, additive (alpha untouched)
|
|
float q = max(d, 0.0) / v_sw.y;
|
|
float g = exp(-0.5 * q * q) * v_sw.z * (d > 0.0 ? 1.0 : 0.0);
|
|
col.rgb += v_fill.rgb * g;
|
|
}
|
|
o = col * u_alpha;
|
|
}
|
|
'''
|
|
|
|
TEX_VS = '''#version 430
|
|
#include "common.glsl"
|
|
in vec2 a_corner;
|
|
uniform vec4 u_rect; // local quad x0,y0,x1,y1
|
|
uniform vec2 u_t; // translation
|
|
uniform vec4 u_m; // 2x2 matrix (m00, m10, m01, m11)
|
|
out vec2 v_uv;
|
|
void main(){
|
|
vec2 lp = mix(u_rect.xy, u_rect.zw, a_corner);
|
|
vec2 wp = u_t + vec2(u_m.x * lp.x + u_m.z * lp.y, u_m.y * lp.x + u_m.w * lp.y);
|
|
gl_Position = vec4(to_ndc(cam(wp)), 0, 1);
|
|
v_uv = a_corner;
|
|
}
|
|
'''
|
|
|
|
TEX_FS = '''#version 430
|
|
#include "common.glsl"
|
|
in vec2 v_uv;
|
|
uniform sampler2D u_tex;
|
|
uniform vec4 u_color;
|
|
uniform vec4 u_reveal; // u0, u1, softness, mode (0 = horizontal, 1 = vertical)
|
|
uniform int u_mode; // 0 = mask (R channel) coloured, 1 = premultiplied rgba image
|
|
out vec4 o;
|
|
void main(){
|
|
vec4 c;
|
|
if (u_mode == 0) { float m = texture(u_tex, v_uv).r; c = vec4(u_color.rgb, 1.0) * m * u_color.a; }
|
|
else c = texture(u_tex, v_uv) * u_color;
|
|
float x = u_reveal.w > 0.5 ? v_uv.y : v_uv.x;
|
|
float sft = max(u_reveal.z, 1e-4);
|
|
float r = smoothstep(u_reveal.x - sft, u_reveal.x, x) * (1.0 - smoothstep(u_reveal.y, u_reveal.y + sft, x));
|
|
o = c * r * u_alpha;
|
|
}
|
|
'''
|
|
|
|
COMMON = '''
|
|
uniform vec4 u_cam; // centre x, y, zoom, rotation
|
|
uniform vec2 u_shake;
|
|
uniform vec2 u_logical; // 1920, 1080
|
|
uniform float u_alpha;
|
|
const float PI = 3.14159265358979;
|
|
vec2 cam(vec2 p){
|
|
vec2 c = u_logical * 0.5;
|
|
vec2 q = p - u_cam.xy;
|
|
float cs = cos(u_cam.w), sn = sin(u_cam.w);
|
|
q = vec2(cs * q.x - sn * q.y, sn * q.x + cs * q.y) * u_cam.z;
|
|
return q + c + u_shake;
|
|
}
|
|
vec2 uncam(vec2 s){
|
|
vec2 c = u_logical * 0.5;
|
|
vec2 q = (s - c - u_shake) / u_cam.z;
|
|
float cs = cos(-u_cam.w), sn = sin(-u_cam.w);
|
|
return vec2(cs * q.x - sn * q.y, sn * q.x + cs * q.y) + u_cam.xy;
|
|
}
|
|
vec2 to_ndc(vec2 p){ return vec2(p.x / u_logical.x * 2.0 - 1.0, 1.0 - p.y / u_logical.y * 2.0); }
|
|
'''
|
|
|
|
|
|
def _mat(xf, skew):
|
|
"""xf = (tx, ty, rot, scale) or (tx, ty, (m00, m01, m10, m11)); skew = (x shear, y scale)."""
|
|
if len(xf) == 3:
|
|
m00, m01, m10, m11 = xf[2]
|
|
else:
|
|
c, s_ = math.cos(xf[2]) * xf[3], math.sin(xf[2]) * xf[3]
|
|
m00, m01, m10, m11 = c, -s_, s_, c
|
|
sh, sy = skew
|
|
# M * [[1, sh], [0, sy]]
|
|
a, b, c2, d = m00, m00 * sh + m01 * sy, m10, m10 * sh + m11 * sy
|
|
return (a, c2, b, d)
|
|
|
|
|
|
class Engine:
|
|
def __init__(self, scale=1.0, samples=1, shutter=0.5, fps=60, libgl=None):
|
|
libgl = libgl or os.environ.get('MGL_LIBGL') or 'opengl32.dll'
|
|
self.ctx = moderngl.create_context(standalone=True, require=430, libgl=libgl)
|
|
self.renderer = self.ctx.info['GL_RENDERER']
|
|
self.scale = scale
|
|
self.W, self.H = int(LW * scale), int(LH * scale)
|
|
self.samples, self.shutter, self.fps = samples, shutter, fps
|
|
ctx = self.ctx
|
|
self.progs = {}
|
|
self.p_shape = self.program(SHAPE_VS, SHAPE_FS)
|
|
self.p_tex = self.program(TEX_VS, TEX_FS)
|
|
self.quad = ctx.buffer(np.array([0, 0, 1, 0, 0, 1, 1, 1], dtype='f4').tobytes())
|
|
self.inst = ctx.buffer(reserve=4096 * 40 * 4, dynamic=True)
|
|
self.vao_shape = ctx.vertex_array(self.p_shape, [
|
|
(self.quad, '2f', 'a_corner'),
|
|
(self.inst, '4f 4f 4f 4f 4f 4f 4f 4f 4f 4f/i',
|
|
'i_rect', 'i_xf', 'i_kind', 'i_p0', 'i_p1', 'i_p2', 'i_p3', 'i_fill', 'i_stroke', 'i_sw')])
|
|
self.vao_tex = ctx.vertex_array(self.p_tex, [(self.quad, '2f', 'a_corner')])
|
|
self.empty_vao = {}
|
|
# targets
|
|
self.t_scene = ctx.texture((self.W, self.H), 4, dtype='f2')
|
|
self.f_scene = ctx.framebuffer([self.t_scene])
|
|
self.t_acc = ctx.texture((self.W, self.H), 4, dtype='f4')
|
|
self.f_acc = ctx.framebuffer([self.t_acc])
|
|
self.t_out = ctx.texture((self.W, self.H), 4, dtype='f1')
|
|
self.f_out = ctx.framebuffer([self.t_out])
|
|
self.bloom = []
|
|
w, h = self.W, self.H
|
|
for i in range(7):
|
|
w, h = max(w // 2, 1), max(h // 2, 1)
|
|
t = ctx.texture((w, h), 4, dtype='f2')
|
|
t.repeat_x = t.repeat_y = False
|
|
self.bloom.append((t, ctx.framebuffer([t])))
|
|
for t in (self.t_scene, self.t_acc):
|
|
t.repeat_x = t.repeat_y = False
|
|
self.p_acc = self.program(VS_FULL, ACC_FS)
|
|
self.p_bright = self.program(VS_FULL, BRIGHT_FS)
|
|
self.p_down = self.program(VS_FULL, DOWN_FS)
|
|
self.p_up = self.program(VS_FULL, UP_FS)
|
|
self.p_final = self.program(VS_FULL, FINAL_FS)
|
|
self.text_cache = OrderedDict()
|
|
self.img_cache = {}
|
|
self.rt_cache = {}
|
|
self.batch = []
|
|
self.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
|
self.shake = (0.0, 0.0)
|
|
self.alpha = 1.0
|
|
self.logical = (LW, LH)
|
|
self.target_stack = []
|
|
|
|
# ---- programs
|
|
def program(self, vs, fs):
|
|
key = (vs, fs)
|
|
if key not in self.progs:
|
|
self.progs[key] = self.ctx.program(vertex_shader=preprocess(vs), fragment_shader=preprocess(fs))
|
|
return self.progs[key]
|
|
|
|
def fs_program(self, src):
|
|
return self.program(VS_FULL, src)
|
|
|
|
def _set(self, prog, **u):
|
|
for k, v in u.items():
|
|
if k in prog:
|
|
prog[k].value = v
|
|
|
|
def _common(self, prog):
|
|
self._set(prog, u_cam=self.cam, u_shake=self.shake, u_logical=self.logical, u_alpha=self.alpha)
|
|
|
|
# ---- render targets (offscreen layers in logical space)
|
|
def layer(self, name, w=LW, h=LH):
|
|
key = (name, w, h)
|
|
if key not in self.rt_cache:
|
|
t = self.ctx.texture((int(w * self.scale), int(h * self.scale)), 4, dtype='f2')
|
|
t.build_mipmaps()
|
|
t.filter = (moderngl.LINEAR_MIPMAP_LINEAR, moderngl.LINEAR)
|
|
self.rt_cache[key] = (t, self.ctx.framebuffer([t]))
|
|
return self.rt_cache[key]
|
|
|
|
def push_target(self, name, w=LW, h=LH, clear=(0, 0, 0, 0)):
|
|
"""Draw into an offscreen layer of logical size w x h until pop_target(); returns its texture."""
|
|
self.flush()
|
|
t, fb = self.layer(name, w, h)
|
|
self.target_stack.append((fb, t, self.cam, self.shake, self.logical, self.alpha))
|
|
fb.use()
|
|
fb.clear(*clear)
|
|
self.logical = (w, h)
|
|
self.cam = (w / 2, h / 2, 1.0, 0.0)
|
|
self.shake = (0.0, 0.0)
|
|
self.alpha = 1.0
|
|
return t
|
|
|
|
def pop_target(self):
|
|
self.flush()
|
|
fb, t, self.cam, self.shake, self.logical, self.alpha = self.target_stack.pop()
|
|
t.build_mipmaps()
|
|
prev = self.target_stack[-1][0] if self.target_stack else self.f_scene
|
|
prev.use()
|
|
return t
|
|
|
|
# ---- shapes
|
|
def _shape(self, kind, rect, xf, p, fill, stroke=(0, 0, 0, 0), sw=0.0, glow=0.0, glow_k=0.0, extra=(0, 0)):
|
|
pp = list(p) + [0.0] * (16 - len(p))
|
|
self.batch.append(('s', (rect, xf, (kind, extra[0], extra[1], 0), pp, fill, stroke, (sw, glow, glow_k, 0))))
|
|
|
|
def circle(self, x, y, r, fill, stroke=(0, 0, 0, 0), sw=0.0, glow=0.0, glow_k=0.0):
|
|
self._shape(0, (-r, -r, r, r), (x, y, 0, 1), (r,), fill, stroke, sw, glow, glow_k)
|
|
|
|
def rect(self, x, y, w, h, fill, r=0.0, rot=0.0, stroke=(0, 0, 0, 0), sw=0.0, glow=0.0, glow_k=0.0, s=1.0):
|
|
"""Centred rectangle (x, y = centre) with corner radius r."""
|
|
hw, hh = abs(w) / 2, abs(h) / 2
|
|
r = min(r, hw, hh)
|
|
self._shape(1, (-hw, -hh, hw, hh), (x, y, rot, s), (hw, hh, r), fill, stroke, sw, glow, glow_k)
|
|
|
|
def line(self, x0, y0, x1, y1, width, fill, w1=None, glow=0.0, glow_k=0.0):
|
|
r0 = width / 2
|
|
r1 = (w1 if w1 is not None else width) / 2
|
|
rr = max(r0, r1)
|
|
self._shape(2, (min(x0, x1) - rr, min(y0, y1) - rr, max(x0, x1) + rr, max(y0, y1) + rr), (0, 0, 0, 1),
|
|
(x0, y0, x1, y1, r0, r1), fill, glow=glow, glow_k=glow_k)
|
|
|
|
def poly(self, pts, fill, x=0.0, y=0.0, rot=0.0, s=1.0, stroke=(0, 0, 0, 0), sw=0.0, round_=0.0, glow=0.0,
|
|
glow_k=0.0):
|
|
assert 3 <= len(pts) <= 8
|
|
xs = [p[0] for p in pts]
|
|
ys = [p[1] for p in pts]
|
|
flat = [c for p in pts for c in p]
|
|
self._shape(4, (min(xs) - round_, min(ys) - round_, max(xs) + round_, max(ys) + round_), (x, y, rot, s),
|
|
flat, fill, stroke, sw, glow, glow_k, extra=(len(pts), round_))
|
|
|
|
def arc(self, x, y, r, th, a0, a1, fill, glow=0.0, glow_k=0.0):
|
|
"""Ring segment from angle a0 to a1 (radians, y down -> clockwise)."""
|
|
if a1 - a0 >= 2 * math.pi - 1e-4:
|
|
a0, a1 = -math.pi - 0.01, math.pi + 0.01
|
|
R = r + th
|
|
self._shape(5, (-R, -R, R, R), (x, y, 0, 1), (r, th, a0, a1), fill, glow=glow, glow_k=glow_k)
|
|
|
|
def ring(self, x, y, r, th, fill, glow=0.0, glow_k=0.0):
|
|
self.arc(x, y, r, th, -math.pi, math.pi, fill, glow, glow_k)
|
|
|
|
def polyline(self, pts, width, fill, glow=0.0, glow_k=0.0):
|
|
for a, b in zip(pts, pts[1:]):
|
|
self.line(a[0], a[1], b[0], b[1], width, fill, glow=glow, glow_k=glow_k)
|
|
|
|
# ---- text
|
|
def _text_tex(self, s, font, size, var, tracking):
|
|
fsize = quant_size(size * self.scale)
|
|
k = fsize / (size * self.scale)
|
|
var = {a: round(v / 2) * 2 for a, v in var.items()}
|
|
key = (s, font, round(fsize, 3), tuple(sorted(var.items())), round(tracking * self.scale * k, 2))
|
|
e = self.text_cache.get(key)
|
|
if e is not None:
|
|
self.text_cache.move_to_end(key)
|
|
return e, fsize
|
|
F = face(font)
|
|
glyphs, adv = F.shape(s, fsize, var, tracking * self.scale * k)
|
|
pad = int(fsize * 0.05) + 4
|
|
asc = int(math.ceil(fsize * 1.05))
|
|
desc = int(math.ceil(fsize * 0.35))
|
|
wpx = int(math.ceil(adv)) + 2 * pad + int(fsize * 0.3)
|
|
hpx = asc + desc + 2 * pad
|
|
canvas = np.zeros((hpx, wpx), np.uint8)
|
|
ox, oy = pad + int(fsize * 0.15), pad + asc
|
|
for gid, gx, gy, _ in glyphs:
|
|
bm, left, top = F.raster(gid, fsize, var)
|
|
if bm.size == 0:
|
|
continue
|
|
x0 = int(round(ox + gx)) + left
|
|
y0 = int(round(oy + gy)) - top
|
|
h, w = bm.shape
|
|
xa, ya = max(x0, 0), max(y0, 0)
|
|
xb, yb = min(x0 + w, wpx), min(y0 + h, hpx)
|
|
if xb <= xa or yb <= ya:
|
|
continue
|
|
sub = canvas[ya:yb, xa:xb]
|
|
np.maximum(sub, bm[ya - y0:yb - y0, xa - x0:xb - x0], out=sub)
|
|
tex = self.ctx.texture((wpx, hpx), 1, canvas.tobytes())
|
|
tex.build_mipmaps()
|
|
tex.filter = (moderngl.LINEAR_MIPMAP_LINEAR, moderngl.LINEAR)
|
|
tex.repeat_x = tex.repeat_y = False
|
|
e = (tex, wpx, hpx, ox, oy, adv, F.cap * fsize)
|
|
self.text_cache[key] = e
|
|
if len(self.text_cache) > 900:
|
|
_, old = self.text_cache.popitem(last=False)
|
|
old[0].release()
|
|
return e, fsize
|
|
|
|
def measure(self, s, font='archivo', size=100, wght=700, wdth=100, tracking=0.0):
|
|
F = face(font)
|
|
var = {'wght': wght, 'wdth': wdth} if font != 'mono' else {'wght': wght}
|
|
return F.shape(s, size, var, tracking)[1]
|
|
|
|
def text(self, s, x, y, size=100, color=(1, 1, 1, 1), font='archivo', wght=700, wdth=100, align='center',
|
|
valign='cap', rot=0.0, s_=1.0, tracking=0.0, reveal=(0.0, 1.0, 0.0, 0.0), skew=0.0, sy=1.0, mat=None):
|
|
"""Draw a line of text. (x, y) is the anchor: align = left/center/right on the advance box; valign =
|
|
'cap' (centre of cap height), 'base' (baseline), 'top' (cap top). Returns the advance width."""
|
|
if not s:
|
|
return 0.0
|
|
var = {'wght': wght, 'wdth': wdth} if font != 'mono' else {'wght': wght}
|
|
(tex, wpx, hpx, ox, oy, adv, cap), fsize = self._text_tex(s, font, size, var, tracking)
|
|
k = size / (fsize / self.scale) / self.scale # texture px -> logical px
|
|
adv_l = adv * k
|
|
ax = {'left': 0.0, 'center': adv_l / 2, 'right': adv_l}[align]
|
|
ay = {'base': 0.0, 'cap': -cap * k / 2, 'top': -cap * k}[valign]
|
|
x0 = -ox * k - ax
|
|
y0 = -oy * k - ay
|
|
xf = (x, y, mat) if mat is not None else (x, y, rot, s_)
|
|
self.batch.append(('t', (tex, (x0, y0, x0 + wpx * k, y0 + hpx * k), xf, color, reveal, (skew, sy), 0)))
|
|
return adv_l
|
|
|
|
def image(self, tex, x, y, w, h, rot=0.0, color=(1, 1, 1, 1), s=1.0, reveal=(0.0, 1.0, 0.0, 0.0), mat=None):
|
|
"""Premultiplied RGBA texture (e.g. a pushed layer, which is stored bottom-up) centred at x, y."""
|
|
xf = (x, y, mat) if mat is not None else (x, y, rot, s)
|
|
self.batch.append(('t', (tex, (-w / 2, h / 2, w / 2, -h / 2), xf, color, reveal, (0, 1), 1)))
|
|
|
|
def glyph_runs(self, s, font='archivo', size=100, wght=700, wdth=100, tracking=0.0):
|
|
"""Per-character x offsets (logical px, pen start = 0) and total advance, for per-letter animation."""
|
|
F = face(font)
|
|
var = {'wght': wght, 'wdth': wdth} if font != 'mono' else {'wght': wght}
|
|
glyphs, adv = F.shape(s, size, var, tracking)
|
|
xs = [0.0] * len(s)
|
|
for gid, gx, gy, cl in glyphs:
|
|
xs[cl] = gx
|
|
ends = []
|
|
for i in range(len(s)):
|
|
ends.append(xs[i + 1] if i + 1 < len(s) else adv)
|
|
return xs, ends, adv
|
|
|
|
# ---- fullscreen shader layer
|
|
def fullscreen(self, prog, **uniforms):
|
|
self.flush()
|
|
self._common(prog)
|
|
self._set(prog, u_res=(self.W, self.H), **uniforms)
|
|
vao = self.empty_vao.get(id(prog))
|
|
if vao is None:
|
|
vao = self.empty_vao[id(prog)] = self.ctx.vertex_array(prog, [])
|
|
vao.render(moderngl.TRIANGLES, vertices=3)
|
|
|
|
def bind_textures(self, prog, **tex):
|
|
for i, (name, t) in enumerate(tex.items()):
|
|
t.use(location=i + 1)
|
|
if name in prog:
|
|
prog[name].value = i + 1
|
|
|
|
# ---- batching
|
|
def flush(self):
|
|
if not self.batch:
|
|
return
|
|
ctx = self.ctx
|
|
ctx.enable(moderngl.BLEND)
|
|
ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA)
|
|
i = 0
|
|
while i < len(self.batch):
|
|
kind = self.batch[i][0]
|
|
j = i
|
|
while j < len(self.batch) and self.batch[j][0] == kind and (kind == 's' or j == i):
|
|
j += 1
|
|
if kind == 's':
|
|
rows = []
|
|
for _, (rect, xf, kd, pp, fill, stroke, sw) in self.batch[i:j]:
|
|
rows.append(list(rect) + list(xf) + list(kd) + pp + list(fill) + list(stroke) + list(sw))
|
|
data = np.array(rows, dtype='f4')
|
|
if data.nbytes > self.inst.size:
|
|
self.inst.orphan(data.nbytes * 2)
|
|
self.inst.write(data.tobytes())
|
|
self._common(self.p_shape)
|
|
self.vao_shape.render(moderngl.TRIANGLE_STRIP, vertices=4, instances=len(rows))
|
|
else:
|
|
tex, rect, xf, color, reveal, skew, mode = self.batch[i][1]
|
|
p = self.p_tex
|
|
self._common(p)
|
|
tex.use(location=0)
|
|
p['u_tex'].value = 0
|
|
p['u_rect'].value = rect
|
|
p['u_t'].value = xf[:2]
|
|
p['u_m'].value = _mat(xf, skew)
|
|
p['u_color'].value = color
|
|
p['u_reveal'].value = reveal
|
|
p['u_mode'].value = mode
|
|
self.vao_tex.render(moderngl.TRIANGLE_STRIP, vertices=4)
|
|
i = j
|
|
self.batch = []
|
|
|
|
# ---- frame
|
|
def begin_sub(self, clear):
|
|
self.f_scene.use()
|
|
self.f_scene.clear(*clear)
|
|
self.logical = (LW, LH)
|
|
self.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
|
self.shake = (0.0, 0.0)
|
|
self.alpha = 1.0
|
|
|
|
def accumulate(self, weight, first):
|
|
self.flush()
|
|
ctx = self.ctx
|
|
self.f_acc.use()
|
|
if first:
|
|
self.f_acc.clear(0, 0, 0, 0)
|
|
ctx.enable(moderngl.BLEND)
|
|
ctx.blend_func = (moderngl.ONE, moderngl.ONE)
|
|
self.t_scene.use(location=0)
|
|
self.p_acc['u_src'].value = 0
|
|
self.p_acc['u_w'].value = weight
|
|
self._fs(self.p_acc)
|
|
|
|
def _fs(self, prog):
|
|
vao = self.empty_vao.get(id(prog))
|
|
if vao is None:
|
|
vao = self.empty_vao[id(prog)] = self.ctx.vertex_array(prog, [])
|
|
vao.render(moderngl.TRIANGLES, vertices=3)
|
|
|
|
def post(self, P, frame_idx):
|
|
ctx = self.ctx
|
|
ctx.disable(moderngl.BLEND)
|
|
# bloom
|
|
t0, f0 = self.bloom[0]
|
|
f0.use()
|
|
self.t_acc.use(location=0)
|
|
self.p_bright['u_src'].value = 0
|
|
self.p_bright['u_thresh'].value = P.get('bloom_thresh', 0.9)
|
|
self.p_bright['u_texel'].value = (1 / self.W, 1 / self.H)
|
|
self._fs(self.p_bright)
|
|
for i in range(1, len(self.bloom)):
|
|
src, _ = self.bloom[i - 1]
|
|
dst, fb = self.bloom[i]
|
|
fb.use()
|
|
src.use(location=0)
|
|
self.p_down['u_src'].value = 0
|
|
self.p_down['u_texel'].value = (1 / src.width, 1 / src.height)
|
|
self._fs(self.p_down)
|
|
ctx.enable(moderngl.BLEND)
|
|
ctx.blend_func = (moderngl.ONE, moderngl.ONE)
|
|
for i in range(len(self.bloom) - 1, 0, -1):
|
|
src, _ = self.bloom[i]
|
|
dst, fb = self.bloom[i - 1]
|
|
fb.use()
|
|
src.use(location=0)
|
|
self.p_up['u_src'].value = 0
|
|
self.p_up['u_texel'].value = (1 / src.width, 1 / src.height)
|
|
self._fs(self.p_up)
|
|
ctx.disable(moderngl.BLEND)
|
|
self.f_out.use()
|
|
p = self.p_final
|
|
self.t_acc.use(location=0)
|
|
self.bloom[0][0].use(location=1)
|
|
p['u_src'].value = 0
|
|
p['u_bloom'].value = 1
|
|
self._set(p, u_res=(self.W, self.H), u_scale=self.scale,
|
|
u_bloom_k=P.get('bloom', 0.6), u_ca=P.get('ca', 0.0), u_vig=P.get('vignette', 0.35),
|
|
u_grain=P.get('grain', 0.045) * float(os.environ.get('GRAIN_K', '0.6')), u_flash=P.get('flash', (0, 0, 0, 0)), u_invert=P.get('invert', 0.0),
|
|
u_frame=float(frame_idx), u_exposure=P.get('exposure', 1.0), u_fade=P.get('fade', 0.0),
|
|
u_scan=P.get('scan', 0.0), u_grain_rate=float(os.environ.get('GRAIN_RATE', '0')))
|
|
self._fs(p)
|
|
|
|
def read(self):
|
|
return self.t_out.read()
|
|
|
|
|
|
ACC_FS = '''#version 430
|
|
in vec2 v_uv; uniform sampler2D u_src; uniform float u_w; out vec4 o;
|
|
void main(){ o = texture(u_src, v_uv) * u_w; }
|
|
'''
|
|
|
|
BRIGHT_FS = '''#version 430
|
|
in vec2 v_uv; uniform sampler2D u_src; uniform float u_thresh; uniform vec2 u_texel; out vec4 o;
|
|
void main(){
|
|
vec3 c = vec3(0);
|
|
for (int i = 0; i < 4; i++){ vec2 d = vec2((i & 1) == 0 ? -0.5 : 0.5, (i & 2) == 0 ? -0.5 : 0.5) * u_texel;
|
|
c += texture(u_src, v_uv + d).rgb; }
|
|
c *= 0.25;
|
|
float l = max(c.r, max(c.g, c.b));
|
|
float knee = 0.25;
|
|
float s = clamp(l - u_thresh + knee, 0.0, 2.0 * knee);
|
|
s = s * s / (4.0 * knee + 1e-4);
|
|
float w = max(s, l - u_thresh) / max(l, 1e-4);
|
|
o = vec4(min(c * w, vec3(40.0)), 1);
|
|
}
|
|
'''
|
|
|
|
DOWN_FS = '''#version 430
|
|
in vec2 v_uv; uniform sampler2D u_src; uniform vec2 u_texel; out vec4 o;
|
|
void main(){
|
|
vec2 t = u_texel;
|
|
vec3 a = texture(u_src, v_uv + t * vec2(-2, -2)).rgb, b = texture(u_src, v_uv + t * vec2(0, -2)).rgb,
|
|
c = texture(u_src, v_uv + t * vec2(2, -2)).rgb, d = texture(u_src, v_uv + t * vec2(-2, 0)).rgb,
|
|
e = texture(u_src, v_uv).rgb, f = texture(u_src, v_uv + t * vec2(2, 0)).rgb,
|
|
g = texture(u_src, v_uv + t * vec2(-2, 2)).rgb, h = texture(u_src, v_uv + t * vec2(0, 2)).rgb,
|
|
i = texture(u_src, v_uv + t * vec2(2, 2)).rgb, j = texture(u_src, v_uv + t * vec2(-1, -1)).rgb,
|
|
k = texture(u_src, v_uv + t * vec2(1, -1)).rgb, l = texture(u_src, v_uv + t * vec2(-1, 1)).rgb,
|
|
m = texture(u_src, v_uv + t * vec2(1, 1)).rgb;
|
|
vec3 r = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
|
|
o = vec4(r, 1);
|
|
}
|
|
'''
|
|
|
|
UP_FS = '''#version 430
|
|
in vec2 v_uv; uniform sampler2D u_src; uniform vec2 u_texel; out vec4 o;
|
|
void main(){
|
|
vec2 t = u_texel;
|
|
vec3 r = texture(u_src, v_uv).rgb * 4.0;
|
|
r += (texture(u_src, v_uv + vec2(-t.x, 0)).rgb + texture(u_src, v_uv + vec2(t.x, 0)).rgb +
|
|
texture(u_src, v_uv + vec2(0, -t.y)).rgb + texture(u_src, v_uv + vec2(0, t.y)).rgb) * 2.0;
|
|
r += texture(u_src, v_uv + vec2(-t.x, -t.y)).rgb + texture(u_src, v_uv + vec2(t.x, -t.y)).rgb +
|
|
texture(u_src, v_uv + vec2(-t.x, t.y)).rgb + texture(u_src, v_uv + vec2(t.x, t.y)).rgb;
|
|
o = vec4(r / 16.0 * 0.85, 1);
|
|
}
|
|
'''
|
|
|
|
FINAL_FS = '''#version 430
|
|
in vec2 v_uv; uniform sampler2D u_src; uniform sampler2D u_bloom; uniform vec2 u_res; uniform float u_scale;
|
|
uniform float u_grain_rate, u_bloom_k, u_ca, u_vig, u_grain, u_invert, u_frame, u_exposure, u_fade, u_scan; uniform vec4 u_flash;
|
|
out vec4 o;
|
|
uint pcg(uint v){ uint s = v * 747796405u + 2891336453u; uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;
|
|
return (w >> 22u) ^ w; }
|
|
float hash(vec3 p){ // integer hash: bit-identical on every GL implementation
|
|
uvec3 q = uvec3(ivec3(p) + 65536);
|
|
return float(pcg(q.x + pcg(q.y + pcg(q.z))) & 0xffffffu) / 16777216.0; }
|
|
vec3 tonemap(vec3 c){
|
|
// linear below 0.8, smooth shoulder to 1
|
|
vec3 k = max(c - 0.8, 0.0);
|
|
return min(c, 0.8) + 0.2 * (1.0 - exp(-k / 0.2));
|
|
}
|
|
void main(){
|
|
vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y); // flip so the readback is top row first
|
|
vec2 d = uv - 0.5;
|
|
vec3 c;
|
|
if (u_ca > 0.0) {
|
|
vec2 off = d * u_ca * 0.012;
|
|
c = vec3(texture(u_src, uv + off).r, texture(u_src, uv).g, texture(u_src, uv - off).b);
|
|
} else c = texture(u_src, uv).rgb;
|
|
c += texture(u_bloom, uv).rgb * u_bloom_k;
|
|
c *= u_exposure;
|
|
c = tonemap(c);
|
|
c = mix(c, vec3(1.0) - c, u_invert);
|
|
c = mix(c, u_flash.rgb, u_flash.a);
|
|
float v = 1.0 - u_vig * pow(length(d * vec2(1.0, 0.75)) * 1.25, 2.4);
|
|
c *= clamp(v, 0.0, 1.0);
|
|
if (u_scan > 0.0) c *= 1.0 - u_scan * (0.5 + 0.5 * sin(uv.y * u_res.y * 3.14159 / max(u_scale * 2.0, 1.0)));
|
|
c *= 1.0 - u_fade;
|
|
vec2 px = floor(vec2(gl_FragCoord.x, u_res.y - gl_FragCoord.y) / max(u_scale, 0.5));
|
|
float gf = u_grain_rate > 0.0 ? floor(u_frame / u_grain_rate) : 0.0; // 0 = a fixed print texture
|
|
float g = hash(vec3(px, mod(gf, 97.0))) + hash(vec3(px + 17.0, mod(gf, 89.0))) - 1.0;
|
|
float lum = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
c += g * u_grain * (0.35 + 0.65 * (1.0 - lum)) * (0.6 + 0.4 * lum);
|
|
c = clamp(c, 0.0, 1.0);
|
|
vec3 s = mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
|
|
s += (hash(vec3(px, mod(gf, 61.0) + 3.0)) - 0.5) / 255.0;
|
|
o = vec4(s, 1);
|
|
}
|
|
'''
|