Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions pygfx/renderers/wgpu/_flusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,14 @@ def __init__(self, target_format):
self._bind_group = None
self._bind_group_hash = None

def render(self, src_color_tex, src_depth_tex, dst_color_tex, gamma=1.0):
def render(
self,
src_color_tex,
src_depth_tex,
dst_color_tex,
gamma=1.0,
filter_strength=1.0,
):
"""Render the (internal) result of the renderer to a texture view."""

# NOTE: src_depth_tex is not used yet, see #492
Expand All @@ -158,30 +165,28 @@ def render(self, src_color_tex, src_depth_tex, dst_color_tex, gamma=1.0):
)

# Ready to go!
self._update_uniforms(src_color_tex, dst_color_tex, gamma)
self._update_uniforms(src_color_tex, dst_color_tex, gamma, filter_strength)
return self._render(dst_color_tex)

def _update_uniforms(self, src_color_tex, dst_color_tex, gamma):
def _update_uniforms(self, src_color_tex, dst_color_tex, gamma, filter_strength):
# Get factor between texture sizes
factor_x = src_color_tex.size[0] / dst_color_tex.size[0]
factor_y = src_color_tex.size[1] / dst_color_tex.size[1]
factor = (factor_x + factor_y) / 2

if factor == 1:
# With equal res, we smooth a tiny bit.
# A bit less crisp, but also less flicker.
sigma = 0.5
support = 2
# With equal res, we smooth a tiny bit. A bit less crisp, but also less flicker.
ref_sigma = 0.5
elif factor > 1:
# With src a higher res, we will do ssaa.
# Kernel scales with input res.
sigma = 0.5 * factor
support = min(5, int(sigma * 3))
# With src a higher res, we will do SSAA.
ref_sigma = 0.5 * factor
else:
# With src a lower res, the output is interpolated.
# But we also smooth to reduce the blockiness.
sigma = 0.5
support = 2
# With src a lower res, the output is interpolated. But we also smooth to reduce blockiness.
ref_sigma = 0.5

# Determine kernel sigma and support
sigma = ref_sigma * float(filter_strength)
support = int(sigma * 3) # is limited in shader

# Compose
self._uniform_data["size"] = src_color_tex.size[:2]
Expand Down Expand Up @@ -232,7 +237,8 @@ def _create_pipeline(self):

fragment_code = """
// Get info about the smoothing
let sigma = u_render.sigma;
// The limits here may give the compiler info on max iters of the loop below.
let sigma = max(0.1, u_render.sigma);
let support = min(5, u_render.support);

// The reference index is the subpixel index in the source texture that
Expand Down
81 changes: 52 additions & 29 deletions pygfx/renderers/wgpu/_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class WgpuRenderer(RootEventHandler, Renderer):
buffer to a display buffer. If smaller than 1, pixels from the render
buffer are replicated while converting to a display buffer. This has
positive performance implications.
pixel_filter : float, optional
The strength of the filter when copying the result to the target/canvas.
show_fps : bool
Whether to display the frames per second. Beware that
depending on the GUI toolkit, the canvas may impose a frame rate limit.
Expand Down Expand Up @@ -115,6 +117,7 @@ def __init__(
target,
*args,
pixel_ratio=None,
pixel_filter=None,
show_fps=False,
blend_mode="default",
sort_objects=False,
Expand All @@ -131,6 +134,7 @@ def __init__(
)
self._target = target
self.pixel_ratio = pixel_ratio
self.pixel_filter = pixel_filter

# Make sure we have a shared object (the first renderer creates the instance)
self._shared = get_shared()
Expand Down Expand Up @@ -199,29 +203,66 @@ def pixel_ratio(self):
"""The ratio between the number of internal pixels versus the logical pixels on the canvas.

This can be used to configure the size of the render texture
relative to the canvas' logical size. By default (value is None) the
used pixel ratio follows the screens pixel ratio on high-res
displays, and is 2 otherwise.
relative to the canvas' *logical* size. Can be set to None to
set the default. By default the pixel_ratio is 2 on "regular"
screens, and the same as the screen pixel ratio on HiDPI screens
(usually also 2).

If the used pixel ratio causes the render texture to be larger
than the physical size of the canvas, SSAA is applied, resulting
in a smoother final image with less jagged edges. Alternatively,
this value can be set to e.g. 0.5 to lower* the resolution (e.g.
for performance during interaction).
than the physical size of the canvas, SSAA (super sampling
antialiasing) is applied, resulting in a smoother final image
with less jagged edges. Alternatively, this value can be set
to e.g. 0.5 to *lower* the resolution.
"""
return self._pixel_ratio

@pixel_ratio.setter
def pixel_ratio(self, value):
if not value:
value = None
if value is None:
self._pixel_ratio = None
# Get target sizes
target = self._target
if isinstance(target, wgpu.gui.WgpuCanvasBase):
target_psize = target.get_physical_size()
elif isinstance(target, Texture):
target_psize = target.size[:2]
else:
raise TypeError(f"Unexpected render target {target.__class__.__name__}")
target_lsize = self.logical_size
# Determine target ratio
target_ratio = target_psize[0] / target_lsize[0]
# Use 2 on non-hidpi displays. On hidpi displays follow target.
self._pixel_ratio = float(target_ratio) if target_ratio > 1 else 2.0
elif isinstance(value, (int, float)):
self._pixel_ratio = None if value <= 0 else float(value)
self._pixel_ratio = abs(float(value))
else:
raise TypeError(
f"Rendered.pixel_ratio expected None or number, not {value}"
)

@property
def pixel_filter(self):
"""The strength of the filter applied to the final pixels.

The renderer renders everything to an internal texture, which,
depending on the `pixel_ratio`, may have a differens size than
the target (i.e. canvas). In the process of rendering the result
to the target, a filter is applied, resulting in SSAA if the
size was larger, and a smoothing effect otherwise.

When the `pixel_filter` is 1.0, the default optimal filter is
used. Higher values result in more blur. Can be set to 0 to
disable the filter.
"""
return self._pixel_filter

@pixel_filter.setter
def pixel_filter(self, value):
if value is None:
value = 1.0
self._pixel_filter = max(0.0, float(value))

@property
def rect(self):
"""The rectangular viewport for the renderer area."""
Expand All @@ -241,27 +282,8 @@ def logical_size(self):
@property
def physical_size(self):
"""The physical size of the internal render texture."""

# Get physical size of the target
target = self._target
if isinstance(target, wgpu.gui.WgpuCanvasBase):
target_psize = target.get_physical_size()
elif isinstance(target, Texture):
target_psize = target.size[:2]
else:
raise TypeError(f"Unexpected render target {target.__class__.__name__}")

pixel_ratio = self._pixel_ratio
target_lsize = self.logical_size

# Determine the pixel ratio of the render texture
if self._pixel_ratio:
pixel_ratio = self._pixel_ratio
else:
pixel_ratio = target_psize[0] / target_lsize[0]
if pixel_ratio <= 1:
pixel_ratio = 2.0 # use 2 on non-hidpi displays

# Determine the physical size of the internal render textures
return tuple(max(1, int(pixel_ratio * x)) for x in target_lsize)

@property
Expand Down Expand Up @@ -565,6 +587,7 @@ def flush(self, target=None):
None,
wgpu_tex_view,
self._gamma_correction * self._gamma_correction_srgb,
self._pixel_filter,
)
self._device.queue.submit(command_buffers)

Expand Down