Skip to content

Commit

Permalink
Add vflip for textures (#26)
Browse files Browse the repository at this point in the history
* Add vflip

* Update texture tests

* Fix greyscale textures

* Update readme

* revert some type hints for 3.8
  • Loading branch information
Vipitis authored Mar 13, 2024
1 parent 684c28a commit c59a456
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 52 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ Texture inputs are supported by using the `ShadertoyChannel` class. Up to 4 chan
```python
from wgpu_shadertoy import Shadertoy, ShadertoyChannel
from PIL import Image
import numpy as np

shader_code = """
void mainImage( out vec4 fragColor, in vec2 fragCoord )
Expand All @@ -56,8 +55,8 @@ void mainImage( out vec4 fragColor, in vec2 fragCoord )
}
"""

image_data = np.array(Image.open("./examples/screenshots/shadertoy_star.png"))
channel0 = ShadertoyChannel(image_data, wrap="repeat")
img = Image.open("./examples/screenshots/shadertoy_star.png")
channel0 = ShadertoyChannel(img, wrap="repeat")
shader = Shadertoy(shader_code, resolution=(800, 450), inputs=[channel0])
```

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ select = [
]
extend-ignore = [
"E501", # line too long
"RUF005", # + inside concatenation messes with numpy
]

[tool.ruff.lint.per-file-ignores]
Expand Down
44 changes: 16 additions & 28 deletions tests/test_textures.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_textures_wgsl():
bytearray((i for i in range(0, 255, 8) for _ in range(4))) * 32
).cast("B", shape=[32, 32, 4])

channel0 = ShadertoyChannel(test_pattern, wrap="repeat")
channel0 = ShadertoyChannel(test_pattern, wrap="repeat", vflip=False)
channel1 = ShadertoyChannel(gradient)

shader = Shadertoy(
Expand All @@ -36,10 +36,10 @@ def test_textures_wgsl():
assert shader.shader_code == shader_code_wgsl
assert shader.shader_type == "wgsl"
assert shader.inputs[0] == channel0
assert shader.inputs[0].data == test_pattern
assert np.array_equal(shader.inputs[0].data, test_pattern)
assert shader.inputs[0].sampler_settings["address_mode_u"] == "repeat"
assert shader.inputs[1] == channel1
assert shader.inputs[1].data == gradient
assert np.array_equal(shader.inputs[1].data, gradient)
assert shader.inputs[1].sampler_settings["address_mode_u"] == "clamp-to-edge"

shader._draw_frame()
Expand All @@ -66,18 +66,18 @@ def test_textures_glsl():
bytearray((i for i in range(0, 255, 8) for _ in range(4))) * 32
).cast("B", shape=[32, 32, 4])

channel0 = ShadertoyChannel(test_pattern, wrap="repeat")
channel0 = ShadertoyChannel(test_pattern, wrap="repeat", vflip="false")
channel1 = ShadertoyChannel(gradient)

shader = Shadertoy(shader_code, resolution=(640, 480), inputs=[channel0, channel1])
assert shader.resolution == (640, 480)
assert shader.shader_code == shader_code
assert shader.shader_type == "glsl"
assert shader.inputs[0] == channel0
assert shader.inputs[0].data == test_pattern
assert np.array_equal(shader.inputs[0].data, test_pattern)
assert shader.inputs[0].sampler_settings["address_mode_u"] == "repeat"
assert shader.inputs[1] == channel1
assert shader.inputs[1].data == gradient
assert np.array_equal(shader.inputs[1].data, gradient)
assert shader.inputs[1].sampler_settings["address_mode_u"] == "clamp-to-edge"

shader._draw_frame()
Expand Down Expand Up @@ -105,18 +105,12 @@ def test_channel_res_wgsl():
return c0123;
}
"""
img_data = np.array(Image.open("./examples/screenshots/shadertoy_star.png"))
channel0 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 0)), wrap="clamp"
)
channel1 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 1)), wrap="clamp"
)
channel2 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 2)), wrap="repeat"
)
img = Image.open("./examples/screenshots/shadertoy_star.png")
channel0 = ShadertoyChannel(img.rotate(0, expand=True), wrap="clamp", vflip=True)
channel1 = ShadertoyChannel(img.rotate(90, expand=True), wrap="clamp", vflip=False)
channel2 = ShadertoyChannel(img.rotate(180, expand=True), wrap="repeat", vflip=True)
channel3 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 3)), wrap="repeat"
img.rotate(270, expand=True), wrap="repeat", vflip=False
)
shader = Shadertoy(
shader_code_wgsl,
Expand Down Expand Up @@ -171,18 +165,12 @@ def test_channel_res_glsl():
fragColor = c0123;
}
"""
img_data = np.array(Image.open("./examples/screenshots/shadertoy_star.png"))
channel0 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 0)), wrap="clamp"
)
channel1 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 1)), wrap="clamp"
)
channel2 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 2)), wrap="repeat"
)
img = Image.open("./examples/screenshots/shadertoy_star.png")
channel0 = ShadertoyChannel(img.rotate(0, expand=True), wrap="clamp", vflip=True)
channel1 = ShadertoyChannel(img.rotate(90, expand=True), wrap="clamp", vflip=False)
channel2 = ShadertoyChannel(img.rotate(180, expand=True), wrap="repeat", vflip=True)
channel3 = ShadertoyChannel(
np.ascontiguousarray(np.rot90(img_data, 3)), wrap="repeat"
img.rotate(270, expand=True), wrap="repeat", vflip=False
)
shader = Shadertoy(
shader_code,
Expand Down
14 changes: 5 additions & 9 deletions wgpu_shadertoy/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import os

import numpy as np
import requests
from PIL import Image
from wgpu import logger
Expand All @@ -11,7 +10,7 @@
HEADERS = {"user-agent": "https://github.com/pygfx/shadertoy script"}


def _get_api_key():
def _get_api_key() -> str:
key = os.environ.get("SHADERTOY_KEY", None)
if key is None:
raise ValueError(
Expand All @@ -31,10 +30,10 @@ def _get_api_key():
return key


def _download_media_channels(inputs):
def _download_media_channels(inputs: list):
"""
Downloads media (currently just textures) from Shadertoy.com and returns a list of `ShadertoyChannel` to be directly used for `inputs`.
Requiers internet connection (API key not required).
Requires internet connection (API key not required).
"""
media_url = "https://www.shadertoy.com"
channels = {}
Expand All @@ -46,11 +45,8 @@ def _download_media_channels(inputs):
raise requests.exceptions.HTTPError(
f"Failed to load media {media_url + inp['src']} with status code {response.status_code}"
)
img = Image.open(response.raw).convert("RGBA")
img_data = np.array(img)
channel = ShadertoyChannel(
img_data, kind="texture", wrap=inp["sampler"]["wrap"]
)
img = Image.open(response.raw)
channel = ShadertoyChannel(img, kind="texture", **inp["sampler"])
channels[inp["channel"]] = channel
return list(channels.values())

Expand Down
38 changes: 30 additions & 8 deletions wgpu_shadertoy/inputs.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import ctypes
import numpy as np


class ShadertoyChannel:
"""
Represents a shadertoy channel. It can be a texture.
Parameters:
data (array-like): Of shape (width, height, 4), will be converted to memoryview. For example read in your images using ``np.asarray(Image.open("image.png"))``
data (array-like): Of shape (width, height, channels), will be converted to numpy array. Default is a 8x8 black texture.
kind (str): The kind of channel. Can be one of ("texture"). More will be supported in the future
**kwargs: Additional arguments for the sampler:
wrap (str): The wrap mode, can be one of ("clamp-to-edge", "repeat", "clamp"). Default is "clamp-to-edge".
vflip (str or bool): Whether to flip the texture vertically. Can be one of ("true", "false", True, False). Default is True.
"""

# TODO: add cubemap/volume, buffer, webcam, video, audio, keyboard?
Expand All @@ -17,13 +18,30 @@ def __init__(self, data=None, kind="texture", **kwargs):
if kind != "texture":
raise NotImplementedError("Only texture is supported for now.")
if data is not None:
self.data = memoryview(data)
self.data = np.ascontiguousarray(data)
else:
self.data = (
memoryview((ctypes.c_uint8 * 8 * 8 * 4)())
.cast("B")
.cast("B", shape=[8, 8, 4])
self.data = np.zeros((8, 8, 4), dtype=np.uint8)

# if channel dimension is missing, it's a greyscale texture
if len(self.data.shape) == 2:
self.data = np.reshape(self.data, self.data.shape + (1,))
# greyscale textures become just red while green and blue remain 0s
if self.data.shape[2] == 1:
self.data = np.stack(
[
self.data[:, :, 0],
np.zeros_like(self.data[:, :, 0]),
np.zeros_like(self.data[:, :, 0]),
],
axis=-1,
)
# if alpha channel is not given, it's filled with max value (255)
if self.data.shape[2] == 3:
self.data = np.concatenate(
[self.data, np.full(self.data.shape[:2] + (1,), 255, dtype=np.uint8)],
axis=2,
)

self.size = self.data.shape # (rows, columns, channels)
self.texture_size = (
self.data.shape[1],
Expand All @@ -33,6 +51,11 @@ def __init__(self, data=None, kind="texture", **kwargs):
self.bytes_per_pixel = (
self.data.nbytes // self.data.shape[1] // self.data.shape[0]
)
vflip = kwargs.pop("vflip", True)
if vflip in ("true", True):
vflip = True
self.data = np.ascontiguousarray(self.data[::-1, :, :])

self.sampler_settings = {}
wrap = kwargs.pop("wrap", "clamp-to-edge")
if wrap.startswith("clamp"):
Expand All @@ -50,7 +73,6 @@ def __repr__(self):
"shape": self.data.shape,
"strides": self.data.strides,
"nbytes": self.data.nbytes,
"obj": self.data.obj,
}
class_repr = {k: v for k, v in self.__dict__.items() if k != "data"}
class_repr["data"] = data_repr
Expand Down
8 changes: 4 additions & 4 deletions wgpu_shadertoy/shadertoy.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,13 @@ class Shadertoy:

def __init__(
self,
shader_code,
common="",
shader_code: str,
common: str = "",
resolution=(800, 450),
shader_type="auto",
offscreen=None,
inputs=[],
title="Shadertoy",
title: str = "Shadertoy",
) -> None:
self._uniform_data = UniformArray(
("mouse", "f", 4),
Expand Down Expand Up @@ -352,7 +352,7 @@ def shader_code(self) -> str:
return self._shader_code

@property
def shader_type(self):
def shader_type(self) -> str:
"""The shader type, automatically detected from the shader code, can be "wgsl" or "glsl"."""
if self._shader_type in ("wgsl", "glsl"):
return self._shader_type
Expand Down

0 comments on commit c59a456

Please sign in to comment.