Custom image shader that rotates RGB vectors¶
Keywords: image · ImageCanvas · custom shader · uniform · GLSL · RGB · rotation · drag
Replaces the ~pytanga.viz.ImageCanvas standard shader with a custom
fragment shader that rotates each pixel's RGB vector in RGB space around the
grayscale axis (1,1,1)/√3. A custom u_angle uniform drives the
rotation; a left-drag handler maps the cursor's horizontal position to the
angle (0..2π). The shader is registered via
~pytanga.viz.ImageCanvas.register_shader and the uniform via
~pytanga.viz.ImageCanvas.register_uniform.
Run¶
Source¶
viz/image/custom_shader_rgb_rotate.py
Code¶
# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 Christian Perwass
"""custom_shader_rgb_rotate.py — Custom image shader that rotates RGB vectors.
Replaces the :class:`~pytanga.viz.ImageCanvas` standard shader with a custom
fragment shader that rotates each pixel's RGB vector in RGB space around the
grayscale axis ``(1,1,1)/√3``. A custom ``u_angle`` uniform drives the
rotation; a left-drag handler maps the cursor's horizontal position to the
angle (0..2π). The shader is registered via
:meth:`~pytanga.viz.ImageCanvas.register_shader` and the uniform via
:meth:`~pytanga.viz.ImageCanvas.register_uniform`.
Run with: uv run python py/examples/viz/image/custom_shader_rgb_rotate.py
Keywords: image, ImageCanvas, custom shader, uniform, GLSL, RGB, rotation, drag
"""
import math
import numpy as np
from pytanga.viz import (
DragBinding,
DragEvent,
ImageCanvas,
ImageData,
MouseButton,
Visualizer,
)
# Custom fragment shader. The standard vertex shader provides ``vUv``, and the
# renderer always binds ``uImage0`` (the image texture) and ``uImageSize``.
_FRAGMENT = """
precision highp float;
varying vec2 vUv;
uniform sampler2D uImage0;
uniform vec2 uImageSize;
uniform float u_angle;
void main() {
// Nearest-neighbour sample in the pixel frame (no zoom interpolation).
vec2 snap = (floor(vUv * uImageSize) + 0.5) / uImageSize;
vec3 rgb = texture2D(uImage0, snap).rgb;
// Rotate the RGB vector around the grayscale axis (1,1,1)/sqrt(3).
vec3 axis = normalize(vec3(1.0));
float c = cos(u_angle);
float s = sin(u_angle);
vec3 rotated = rgb * c
+ cross(axis, rgb) * s
+ axis * dot(axis, rgb) * (1.0 - c);
gl_FragColor = vec4(clamp(rotated, 0.0, 1.0), 1.0);
}
"""
def _gradient(width: int, height: int) -> np.ndarray:
"""A 3-channel RGB gradient of shape (H, W, 3), dtype uint8."""
ys, xs = np.mgrid[0:height, 0:width]
r = (xs / max(width - 1, 1) * 255).astype(np.uint8)
g = (ys / max(height - 1, 1) * 255).astype(np.uint8)
b = np.full_like(r, 64)
return np.stack([r, g, b], axis=-1)
def main() -> None:
width, height = 320, 200
viz = Visualizer(add_default_axes=False, add_default_grid=False, space_dim=2)
async def on_drag(event: DragEvent, canvas: ImageCanvas) -> bool:
# Horizontal position → rotation angle in [0, 2π].
px = event.world_position.x
canvas.set_uniform("u_angle", 2.0 * math.pi * px / max(width - 1, 1))
return True
canvas = ImageCanvas(
viz,
drag_handlers=[DragBinding(MouseButton.LEFT, on_drag)],
)
canvas.register_shader(_FRAGMENT)
canvas.register_uniform("u_angle", 0.0)
canvas.set_image(ImageData("gradient", data=_gradient(width, height)))
viz.show(layout=canvas.scene_view())
viz.wait()
if __name__ == "__main__":
main()