from functools import partial
import os
import fury
from fury.decorators import warn_on_args_to_kwargs
from fury.deprecator import deprecate_with_version
from fury.io import load_text
from fury.lib import (
VTK_OBJECT,
Command,
DataObject,
Shader,
calldata_type,
numpy_support,
)
SHADERS_DIR = os.path.join(os.path.dirname(__file__))
SHADERS_EXTS = [
".glsl",
".vert",
".tesc",
".tese",
".geom",
".frag",
".comp",
]
SHADERS_TYPE = {
"vertex": Shader.Vertex,
"geometry": Shader.Geometry,
"fragment": Shader.Fragment,
}
REPLACEMENT_SHADERS_TYPES = {"vertex": Shader.Vertex, "fragment": Shader.Fragment}
SHADERS_BLOCK = {
"position": "//VTK::PositionVC", # frag position in VC
"normal": "//VTK::Normal", # optional normal declaration
"light": "//VTK::Light", # extra lighting parameters
"tcoord": "//VTK::TCoord", # Texture coordinates
"color": "//VTK::Color", # material property values
"clip": "//VTK::Clip", # clipping plane vars
"camera": "//VTK::Camera", # camera and actor matrix values
"prim_id": "//VTK::PrimID", # Apple Bug
"valuepass": "//VTK::ValuePass", # Value raster
"output": "//VTK::Output", # only for geometry shader
"coincident": "//VTK::Coincident", # handle coincident offsets
"zbufer": "//VTK::ZBuffer",
"depth_peeling": "//VTK::DepthPeeling", # Depth Peeling Support
"picking": "//VTK::Picking", # picking support
}
# See [1] for a more extensive list of OpenGL constants
# [1] https://docs.factorcode.org/content/vocab-opengl.gl.html
GL_NUMBERS = {
"GL_ONE": 1,
"GL_ZERO": 0,
"GL_BLEND": 3042,
"GL_ONE_MINUS_SRC_ALPHA": 771,
"GL_SRC_ALPHA": 770,
"GL_DEPTH_TEST": 2929,
"GL_DST_COLOR": 774,
"GL_FUNC_SUBTRACT": 3277,
"GL_CULL_FACE": 2884,
"GL_ALPHA_TEST": 3008,
"GL_CW": 2304,
"GL_CCW": 2305,
"GL_ONE_MINUS_SRC_COLOR": 769,
"GL_SRC_COLOR": 768,
}
[docs]
def compose_shader(glsl_code):
"""Merge GLSL shader code from a list of strings.
Parameters
----------
glsl_code : list of str (code or filenames).
Returns
-------
code : str
GLSL shader code.
"""
if not glsl_code:
return ""
if not all(isinstance(i, str) for i in glsl_code):
raise IOError("The only supported format are string.")
if isinstance(glsl_code, str):
return glsl_code
code = ""
for content in glsl_code:
code += "\n"
code += content
return code
[docs]
def import_fury_shader(shader_file):
"""Import a Fury shader.
Parameters
----------
shader_file : str
Filename of shader. The file must be in the fury/shaders directory and
must have the one of the supported extensions specified by the Khronos
Group
(https://github.com/KhronosGroup/glslang#execution-of-standalone-wrapper).
Returns
-------
code : str
GLSL shader code.
"""
shader_fname = os.path.join(SHADERS_DIR, shader_file)
return load_shader(shader_fname)
[docs]
def load_shader(shader_file):
"""Load a shader from a file.
Parameters
----------
shader_file : str
Full path to a shader file ending with one of the file extensions
defined by the Khronos Group
(https://github.com/KhronosGroup/glslang#execution-of-standalone-wrapper).
Returns
-------
code : str
GLSL shader code.
"""
file_ext = os.path.splitext(os.path.basename(shader_file))[1]
if file_ext not in SHADERS_EXTS:
raise IOError(
'Shader file "{}" does not have one of the supported '
"extensions: {}.".format(shader_file, SHADERS_EXTS)
)
return load_text(shader_file)
[docs]
@deprecate_with_version(
message="Load function has been reimplemented as import_fury_shader.",
since="0.8.1",
until="0.9.0",
)
def load(filename):
"""Load a Fury shader file.
Parameters
----------
filename : str
Filename of the shader file.
Returns
-------
code: str
Shader code.
"""
with open(os.path.join(SHADERS_DIR, filename)) as shader_file:
return shader_file.read()
[docs]
@warn_on_args_to_kwargs()
def shader_to_actor(
actor,
shader_type,
*,
impl_code="",
decl_code="",
block="valuepass",
keep_default=True,
replace_first=True,
replace_all=False,
debug=False,
):
"""Apply your own substitutions to the shader creation process.
A set of string replacements is applied to a shader template. This
function let's apply custom string replacements.
Parameters
----------
actor : vtkActor
Fury actor you want to set the shader code to.
shader_type : str
Shader type: vertex, fragment
impl_code : str, optional
Shader implementation code, should be a string or filename. Default
None.
decl_code : str, optional
Shader declaration code, should be a string or filename. Default None.
block : str, optional
Section name to be replaced. VTK use of heavy string replacements to
insert shader and make it flexible. Each section of the shader
template have a specific name. For more information:
https://vtk.org/Wiki/Shaders_In_VTK. The possible values are:
position, normal, light, tcoord, color, clip, camera, prim_id,
valuepass. by default valuepass
keep_default : bool, optional
Keep the default block tag to let VTK replace it with its default
behavior. Default True.
replace_first : bool, optional
If True, apply this change before the standard VTK replacements.
Default True.
replace_all : bool, optional
[description], by default False
debug : bool, optional
Introduce a small error to debug shader code. Default False.
"""
shader_type = shader_type.lower()
shader_type = REPLACEMENT_SHADERS_TYPES.get(shader_type, None)
if shader_type is None:
msg = "Invalid Shader Type. Please choose between "
msg += ", ".join(REPLACEMENT_SHADERS_TYPES.keys())
raise ValueError(msg)
block = block.lower()
block = SHADERS_BLOCK.get(block, None)
if block is None:
msg = "Invalid Shader Type. Please choose between "
msg += ", ".join(SHADERS_BLOCK.keys())
raise ValueError(msg)
block_dec = block + "::Dec"
block_impl = block + "::Impl"
if keep_default:
decl_code = block_dec + "\n" + decl_code
impl_code = block_impl + "\n" + impl_code
if debug:
fury.enable_warnings()
error_msg = "\n\n--- DEBUG: THIS LINE GENERATES AN ERROR ---\n\n"
impl_code += error_msg
sp = actor.GetShaderProperty()
sp.AddShaderReplacement(
shader_type, block_dec, replace_first, decl_code, replace_all
)
sp.AddShaderReplacement(
shader_type, block_impl, replace_first, impl_code, replace_all
)
[docs]
def replace_shader_in_actor(actor, shader_type, code):
"""Set and replace the shader template with a new one.
Parameters
----------
actor : vtkActor
Fury actor you want to set the shader code to.
shader_type : str
Shader type: vertex, geometry, fragment.
code : str
New shader template code.
"""
function_name = {
"vertex": "SetVertexShaderCode",
"fragment": "SetFragmentShaderCode",
"geometry": "SetGeometryShaderCode",
}
shader_type = shader_type.lower()
function = function_name.get(shader_type, None)
if function is None:
msg = "Invalid Shader Type. Please choose between "
msg += ", ".join(function_name.keys())
raise ValueError(msg)
sp = actor.GetShaderProperty()
getattr(sp, function)(code)
[docs]
@warn_on_args_to_kwargs()
def add_shader_callback(actor, callback, *, priority=0.0):
"""Add a shader callback to the actor.
Parameters
----------
actor : vtkActor
Fury actor you want to add the callback to.
callback : callable
Function or class that contains 3 parameters: caller, event, calldata.
This callback will be trigger at each `UpdateShaderEvent` event.
priority : float, optional
Commands with a higher priority are called first.
Returns
-------
id_observer : int
An unsigned Int tag which can be used later to remove the event
or retrieve the vtkCommand used in the observer.
See more at: https://vtk.org/doc/nightly/html/classvtkObject.html
Examples
--------
.. code-block:: python
add_shader_callback(actor, func_call1)
id_observer = add_shader_callback(actor, func_call2)
actor.GetMapper().RemoveObserver(id_observer)
Priority calls
.. code-block:: python
test_values = []
def callbackLow(_caller, _event, calldata=None):
program = calldata
if program is not None:
test_values.append(0)
def callbackHigh(_caller, _event, calldata=None):
program = calldata
if program is not None:
test_values.append(999)
def callbackMean(_caller, _event, calldata=None):
program = calldata
if program is not None:
test_values.append(500)
fs.add_shader_callback(
actor, callbackHigh, priority=999)
fs.add_shader_callback(
actor, callbackLow, priority=0)
id_mean = fs.add_shader_callback(
actor, callbackMean, priority=500)
showm.start()
# test_values = [999, 500, 0, 999, 500, 0, ...]
"""
if not isinstance(priority, (float, int)):
raise ValueError(
"""
add_shader_callback priority argument should be a float/int"""
)
# @warn_on_args_to_kwargs()
@calldata_type(VTK_OBJECT)
def cbk(caller, event, calldata=None):
callback(caller, event, calldata=calldata)
mapper = actor.GetMapper()
id_observer = mapper.AddObserver(Command.UpdateShaderEvent, cbk, priority)
return id_observer
[docs]
@warn_on_args_to_kwargs()
def shader_apply_effects(window, actor, effects, *, priority=0):
"""This applies a specific opengl state (effect) or a list of effects just
before the actor's shader is executed.
Parameters
----------
window : RenderWindow
For example, this is provided by the ShowManager.window attribute.
actor : actor
effects : a function or a list of functions
priority : float, optional
Related with the shader callback command.
Effects with a higher priority are applied first and
can be override by the others.
Returns
-------
id_observer : int
An unsigned Int tag which can be used later to remove the event
or retrieve the vtkCommand used in the observer.
See more at: https://vtk.org/doc/nightly/html/classvtkObject.html
"""
if not isinstance(effects, list):
effects = [effects]
@warn_on_args_to_kwargs()
def callback(_caller, _event, *, calldata=None, effects=None, window=None):
program = calldata
glState = window.GetState()
if program is not None:
for func in effects:
func(glState)
id_observer = add_shader_callback(
actor, partial(callback, effects=effects, window=window), priority=priority
)
return id_observer
[docs]
@warn_on_args_to_kwargs()
def attribute_to_actor(actor, arr, attr_name, *, deep=True):
"""Link a numpy array with vertex attribute.
Parameters
----------
actor : vtkActor
Fury actor you want to add the vertex attribute to.
arr : ndarray
Array to link to vertices.
attr_name : str
Vertex attribute name. The vtk array will take the same name as the
attribute.
deep : bool, optional
If True a deep copy is applied, otherwise a shallow copy is applied.
Default True.
"""
nb_components = arr.shape[1] if arr.ndim > 1 else arr.ndim
vtk_array = numpy_support.numpy_to_vtk(arr, deep=deep)
vtk_array.SetNumberOfComponents(nb_components)
vtk_array.SetName(attr_name)
actor.GetMapper().GetInput().GetPointData().AddArray(vtk_array)
mapper = actor.GetMapper()
mapper.MapDataArrayToVertexAttribute(
attr_name, attr_name, DataObject.FIELD_ASSOCIATION_POINTS, -1
)