update
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
from collections.abc import Iterable
|
||||
from enum import Enum
|
||||
from typing import Literal, cast, overload
|
||||
|
||||
import gi
|
||||
import OpenGL.GL as GL
|
||||
from fabric import Application, Property, Signal
|
||||
from fabric.widgets.widget import Widget
|
||||
from OpenGL.GL.shaders import compileProgram, compileShader
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk
|
||||
|
||||
|
||||
class ShadertoyUniformType(Enum):
|
||||
# TODO: add more types
|
||||
FLOAT = 1
|
||||
INTEGER = 2
|
||||
VECTOR = 3
|
||||
TEXTURE = 4
|
||||
|
||||
|
||||
class ShadertoyCompileError(Exception): ...
|
||||
|
||||
|
||||
class Shadertoy(Gtk.GLArea, Widget):
|
||||
@Signal # pygobject signal
|
||||
def ready(self) -> None: ...
|
||||
|
||||
@Property(str, "read-write")
|
||||
def shader_buffer(self) -> str:
|
||||
return self._shader_buffer
|
||||
|
||||
@shader_buffer.setter
|
||||
def shader_buffer(self, shader_buffer: str) -> None:
|
||||
self._shader_buffer = shader_buffer
|
||||
if not self._ready:
|
||||
return
|
||||
self._shader_uniforms.clear()
|
||||
self.do_realize()
|
||||
self.queue_draw()
|
||||
return
|
||||
|
||||
# signatures for building a replica of shadertoy
|
||||
DEFAULT_VERTEX_SHADER = """
|
||||
#version 330
|
||||
|
||||
in vec2 position;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
DEFAULT_FRAGMENT_UNIFORMS = """
|
||||
#version 330
|
||||
|
||||
uniform vec3 iResolution; // viewport resolution (in pixels)
|
||||
uniform float iTime; // shader playback time (in seconds)
|
||||
uniform float iTimeDelta; // render time (in seconds)
|
||||
uniform float iFrameRate; // shader frame rate
|
||||
uniform int iFrame; // shader playback frame
|
||||
uniform float iChannelTime[4]; // channel playback time (in seconds)
|
||||
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
|
||||
uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
|
||||
uniform sampler2D iChannel0; // input channel. XX = 2D/Cube
|
||||
uniform sampler2D iChannel1;
|
||||
uniform sampler2D iChannel2;
|
||||
uniform sampler2D iChannel3;
|
||||
uniform vec4 iDate; // (year, month, day, time in seconds)
|
||||
uniform float iSampleRate; // sound sample rate (i.e., 44100)
|
||||
|
||||
"""
|
||||
|
||||
FRAGMENT_MAIN_FUNCTION = """
|
||||
void main() {
|
||||
mainImage(gl_FragColor, gl_FragCoord.xy);
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
shader_buffer: str,
|
||||
shader_uniforms: list[
|
||||
tuple[
|
||||
str,
|
||||
ShadertoyUniformType,
|
||||
bool | float | int | tuple[float, ...] | GdkPixbuf.Pixbuf,
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
name: str | None = None,
|
||||
visible: bool = True,
|
||||
all_visible: bool = False,
|
||||
style: str | None = None,
|
||||
style_classes: Iterable[str] | str | None = None,
|
||||
tooltip_text: str | None = None,
|
||||
tooltip_markup: str | None = None,
|
||||
h_align: Literal["fill", "start", "end", "center", "baseline"]
|
||||
| Gtk.Align
|
||||
| None = None,
|
||||
v_align: Literal["fill", "start", "end", "center", "baseline"]
|
||||
| Gtk.Align
|
||||
| None = None,
|
||||
h_expand: bool = False,
|
||||
v_expand: bool = False,
|
||||
size: Iterable[int] | int | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
Gtk.GLArea.__init__(
|
||||
self # type: ignore
|
||||
)
|
||||
Widget.__init__(
|
||||
self,
|
||||
name,
|
||||
visible,
|
||||
all_visible,
|
||||
style,
|
||||
style_classes,
|
||||
tooltip_text,
|
||||
tooltip_markup,
|
||||
h_align,
|
||||
v_align,
|
||||
h_expand,
|
||||
v_expand,
|
||||
size,
|
||||
**kwargs,
|
||||
)
|
||||
self._shader_buffer = shader_buffer
|
||||
self._shader_uniforms = shader_uniforms or []
|
||||
|
||||
# widget settings
|
||||
self.set_required_version(3, 3)
|
||||
self.set_has_depth_buffer(False)
|
||||
self.set_has_stencil_buffer(False)
|
||||
|
||||
self._ready = False
|
||||
self._program = None
|
||||
self._vao = None
|
||||
self._quad_vbo = None
|
||||
self._texture_units = {}
|
||||
|
||||
# timer
|
||||
self._start_time = GLib.get_monotonic_time() / 1e6
|
||||
self._frame_time = self._start_time
|
||||
self._frame_count = 0
|
||||
|
||||
# to avoid a constant framerate we tell
|
||||
# gtk to render a frame whenever possible
|
||||
self._tick_id = self.add_tick_callback(lambda *_: (self.queue_draw(), True)[1])
|
||||
|
||||
def do_bake_program(self):
|
||||
try:
|
||||
vertex_shader = compileShader(
|
||||
self.DEFAULT_VERTEX_SHADER, GL.GL_VERTEX_SHADER
|
||||
)
|
||||
fragment_shader = compileShader(
|
||||
self.DEFAULT_FRAGMENT_UNIFORMS
|
||||
+ self._shader_buffer
|
||||
+ self.FRAGMENT_MAIN_FUNCTION,
|
||||
GL.GL_FRAGMENT_SHADER,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ShadertoyCompileError(
|
||||
f"couldn't compile the provided shader, OpenGL error:\n {e}"
|
||||
)
|
||||
|
||||
return compileProgram(vertex_shader, fragment_shader)
|
||||
|
||||
def do_realize(self, *_):
|
||||
Gtk.GLArea.do_realize(self)
|
||||
if not self._ready:
|
||||
ctx = self.get_context()
|
||||
if (err := self.get_error()) or not ctx:
|
||||
raise RuntimeError(
|
||||
f"couldn't initialize the drawing context, error: {err or 'context is None'}"
|
||||
)
|
||||
|
||||
ctx.make_current()
|
||||
|
||||
if self._program:
|
||||
GL.glDeleteProgram(self._program)
|
||||
self._program = None
|
||||
self._program = self.do_bake_program()
|
||||
|
||||
# NOTE: for this to work (alpha pixels) `self.set_has_alpha(True)` must be done
|
||||
# this breaks some fragment shaders, for some reason, so i'm leaving it for anyone willing to use
|
||||
GL.glEnable(GL.GL_BLEND)
|
||||
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
self._quad_vbo = GL.glGenBuffers(1)
|
||||
GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._quad_vbo)
|
||||
|
||||
# this is not so good, unless the introduction of numpy, we must do
|
||||
# a hack to generate an array GL would accept, i've tried using
|
||||
# the "array" python library but it doesn't seem to be working
|
||||
|
||||
# cast python type into GL type (list[float] -> arraybuf[GLfloat])
|
||||
quad_verts = (-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0)
|
||||
array_type = GL.GLfloat * len(quad_verts)
|
||||
|
||||
GL.glBufferData(
|
||||
GL.GL_ARRAY_BUFFER,
|
||||
len(quad_verts) * 4,
|
||||
array_type(*quad_verts),
|
||||
GL.GL_STATIC_DRAW,
|
||||
)
|
||||
|
||||
self._vao = GL.glGenVertexArrays(1)
|
||||
GL.glBindVertexArray(self._vao)
|
||||
|
||||
position = GL.glGetAttribLocation(self._program, "position")
|
||||
GL.glEnableVertexAttribArray(position)
|
||||
GL.glVertexAttribPointer(position, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, None)
|
||||
|
||||
for uname, utype, uvalue in self._shader_uniforms:
|
||||
self.set_uniform(uname, utype, uvalue) # type: ignore
|
||||
|
||||
self._ready = True
|
||||
self.ready()
|
||||
return
|
||||
|
||||
def do_get_timing(self) -> tuple[float, float, float]:
|
||||
current_time = GLib.get_monotonic_time() / 1e6
|
||||
delta_time = current_time - self._frame_time
|
||||
return current_time, delta_time, (1.0 / delta_time) if delta_time > 0 else 0.0
|
||||
|
||||
def do_post_render(self, time: float):
|
||||
self._frame_time = time
|
||||
self._frame_count += 1
|
||||
return
|
||||
|
||||
def do_render(self, ctx: Gdk.GLContext):
|
||||
if not self._program:
|
||||
if self._tick_id:
|
||||
self.remove_tick_callback(self._tick_id)
|
||||
self._tick_id = 0
|
||||
return False
|
||||
|
||||
GL.glUseProgram(self._program)
|
||||
|
||||
# clear up for next frame
|
||||
GL.glClear(GL.GL_COLOR_BUFFER_BIT)
|
||||
|
||||
alloc = self.get_allocation()
|
||||
width: int = alloc.width # type: ignore
|
||||
height: int = alloc.height # type: ignore
|
||||
mouse_pos = cast(tuple[int, int], self.get_pointer())
|
||||
|
||||
current_time, delta_time, frame_rate = self.do_get_timing()
|
||||
|
||||
self.set_uniform(
|
||||
"iTime", ShadertoyUniformType.FLOAT, current_time - self._start_time
|
||||
)
|
||||
self.set_uniform("iFrame", ShadertoyUniformType.INTEGER, self._frame_count)
|
||||
self.set_uniform("iTimeDelta", ShadertoyUniformType.FLOAT, delta_time)
|
||||
self.set_uniform("iFrameRate", ShadertoyUniformType.FLOAT, frame_rate)
|
||||
self.set_uniform(
|
||||
"iResolution", ShadertoyUniformType.VECTOR, (width, height, 1.0)
|
||||
)
|
||||
self.set_uniform(
|
||||
"iMouse",
|
||||
ShadertoyUniformType.VECTOR,
|
||||
(mouse_pos[0], height - mouse_pos[1], 0, 0),
|
||||
)
|
||||
|
||||
# paint the quad
|
||||
GL.glBindVertexArray(self._vao)
|
||||
GL.glDrawArrays(GL.GL_TRIANGLE_STRIP, 0, 4)
|
||||
self.do_post_render(current_time)
|
||||
return True
|
||||
|
||||
def do_resize(self, width: int, height: int):
|
||||
Gtk.GLArea.do_resize(self, width, height)
|
||||
GL.glViewport(0, 0, width, height)
|
||||
return
|
||||
|
||||
@overload
|
||||
def set_uniform(
|
||||
self, name: str, type: Literal[ShadertoyUniformType.FLOAT], value: float
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def set_uniform(
|
||||
self, name: str, type: Literal[ShadertoyUniformType.INTEGER], value: int
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def set_uniform(
|
||||
self,
|
||||
name: str,
|
||||
type: Literal[ShadertoyUniformType.VECTOR],
|
||||
value: tuple[float, ...],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def set_uniform(
|
||||
self,
|
||||
name: str,
|
||||
type: Literal[ShadertoyUniformType.TEXTURE],
|
||||
value: GdkPixbuf.Pixbuf,
|
||||
): ...
|
||||
|
||||
def set_uniform(
|
||||
self,
|
||||
name: str,
|
||||
type: ShadertoyUniformType,
|
||||
value: bool | float | int | tuple[float, ...] | GdkPixbuf.Pixbuf,
|
||||
):
|
||||
if not self._program:
|
||||
raise RuntimeError("the shader program is not initialized")
|
||||
GL.glUseProgram(self._program)
|
||||
location = GL.glGetUniformLocation(self._program, name)
|
||||
match type:
|
||||
case ShadertoyUniformType.VECTOR:
|
||||
value = cast(tuple[float, ...], value)
|
||||
(
|
||||
GL.glUniform2f
|
||||
if (vlen := len(value)) == 2
|
||||
else GL.glUniform3f
|
||||
if vlen == 3
|
||||
else GL.glUniform4f
|
||||
)(location, *value)
|
||||
case ShadertoyUniformType.FLOAT:
|
||||
GL.glUniform1f(location, value)
|
||||
case ShadertoyUniformType.INTEGER:
|
||||
GL.glUniform1i(location, value)
|
||||
case ShadertoyUniformType.TEXTURE:
|
||||
# who dislikes boilerplate?
|
||||
value = cast(GdkPixbuf.Pixbuf, value).flip(False)
|
||||
format = GL.GL_RGBA if value.get_has_alpha() else GL.GL_RGB
|
||||
|
||||
if name not in self._texture_units:
|
||||
texture = GL.glGenTextures(1)
|
||||
self._texture_units[name] = (len(self._texture_units), texture)
|
||||
else:
|
||||
texture_unit, texture = self._texture_units[name]
|
||||
|
||||
texture_unit = self._texture_units[name][0]
|
||||
GL.glActiveTexture(GL.GL_TEXTURE0 + texture_unit)
|
||||
GL.glBindTexture(GL.GL_TEXTURE_2D, texture)
|
||||
|
||||
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
|
||||
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
|
||||
GL.glTexParameteri(
|
||||
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR
|
||||
)
|
||||
GL.glTexParameteri(
|
||||
GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR
|
||||
)
|
||||
|
||||
# "upload" the texture
|
||||
GL.glTexImage2D(
|
||||
GL.GL_TEXTURE_2D,
|
||||
0, # detail level (woah?)
|
||||
format, # result format
|
||||
value.get_width(),
|
||||
value.get_height(),
|
||||
0, # "border"
|
||||
format, # input format
|
||||
GL.GL_UNSIGNED_BYTE,
|
||||
value.get_pixels(),
|
||||
)
|
||||
GL.glGenerateMipmap(GL.GL_TEXTURE_2D)
|
||||
|
||||
# all aboard...
|
||||
GL.glUniform1i(location, texture_unit)
|
||||
Reference in New Issue
Block a user