diff --git a/examples/basics/scene/mesh_texture.py b/examples/basics/scene/mesh_texture.py new file mode 100644 index 0000000000..155f85c962 --- /dev/null +++ b/examples/basics/scene/mesh_texture.py @@ -0,0 +1,62 @@ +import argparse + +import numpy as np +from vispy import app, scene +from vispy.io import imread, load_data_file, read_mesh +from vispy.scene.visuals import Mesh +from vispy.visuals.filters import TextureFilter + + +parser = argparse.ArgumentParser() +parser.add_argument('--shading', default='smooth', + choices=['none', 'flat', 'smooth'], + help="shading mode") +args = parser.parse_args() + +mesh_path = load_data_file('spot/spot.obj.gz') +texture_path = load_data_file('spot/spot.png') +vertices, faces, normals, texcoords = read_mesh(mesh_path) +texture = np.flipud(imread(texture_path)) + +canvas = scene.SceneCanvas(keys='interactive', bgcolor='white', + size=(800, 600)) +view = canvas.central_widget.add_view() + +view.camera = 'arcball' +# Adapt the depth to the scale of the mesh to avoid rendering artefacts. +view.camera.depth_value = 10 * (vertices.max() - vertices.min()) + +shading = None if args.shading == 'none' else args.shading +mesh = Mesh(vertices, faces, shading=shading, color='white') +mesh.shininess = 1e-3 +view.add(mesh) + +texture_filter = TextureFilter(texture, texcoords) +mesh.attach(texture_filter) + + +@canvas.events.key_press.connect +def on_key_press(event): + if event.key == "t": + texture_filter.enabled = not texture_filter.enabled + mesh.update() + + +def attach_headlight(mesh, view, canvas): + light_dir = (0, -1, 0, 0) + mesh.light_dir = light_dir[:3] + initial_light_dir = view.camera.transform.imap(light_dir) + + @view.scene.transform.changed.connect + def on_transform_change(event): + transform = view.camera.transform + mesh.light_dir = transform.map(initial_light_dir)[:3] + + +attach_headlight(mesh, view, canvas) + + +canvas.show() + +if __name__ == "__main__": + app.run() diff --git a/vispy/visuals/filters/__init__.py b/vispy/visuals/filters/__init__.py index d7d25cf116..93edec0df8 100644 --- a/vispy/visuals/filters/__init__.py +++ b/vispy/visuals/filters/__init__.py @@ -2,6 +2,8 @@ # Copyright (c) Vispy Development Team. All Rights Reserved. # Distributed under the (new) BSD License. See LICENSE.txt for more info. +from .base_filter import Filter # noqa from .clipper import Clipper # noqa from .color import Alpha, ColorFilter, IsolineFilter, ZColormapFilter # noqa from .picking import PickingFilter # noqa +from .mesh import TextureFilter # noqa diff --git a/vispy/visuals/filters/base_filter.py b/vispy/visuals/filters/base_filter.py index 4cfc22721e..a76e91c8de 100644 --- a/vispy/visuals/filters/base_filter.py +++ b/vispy/visuals/filters/base_filter.py @@ -34,14 +34,14 @@ class Filter(BaseFilter): Parameters ---------- - vcode : str | None + vcode : str | Function | None Vertex shader code. If None, ``vhook`` and ``vpos`` will be ignored. vhook : {'pre', 'post'} Hook name to attach the vertex shader to. vpos : int Position in the hook to attach the vertex shader. - fcode : str | None + fcode : str | Function | None Fragment shader code. If None, ``fhook`` and ``fpos`` will be ignored. fhook : {'pre', 'post'} @@ -61,7 +61,7 @@ def __init__(self, vcode=None, vhook='post', vpos=5, super(Filter, self).__init__() if vcode is not None: - self.vshader = Function(vcode) + self.vshader = Function(vcode) if isinstance(vcode, str) else vcode self._vexpr = self.vshader() self._vhook = vhook self._vpos = vpos @@ -69,13 +69,15 @@ def __init__(self, vcode=None, vhook='post', vpos=5, self.vshader = None if fcode is not None: - self.fshader = Function(fcode) + self.fshader = Function(fcode) if isinstance(fcode, str) else fcode self._fexpr = self.fshader() self._fhook = fhook self._fpos = fpos else: self.fshader = None + self._attached = False + def _attach(self, visual): """Called when a filter should be attached to a visual. @@ -92,6 +94,9 @@ def _attach(self, visual): hook = visual._get_hook('frag', self._fhook) hook.add(self._fexpr, position=self._fpos) + self._attached = True + self._visual = visual + def _detach(self, visual): """Called when a filter should be detached from a visual. @@ -107,3 +112,6 @@ def _detach(self, visual): if self.fshader: hook = visual._get_hook('frag', self._fhook) hook.remove(self._fexpr) + + self._attached = False + self._visual = None diff --git a/vispy/visuals/filters/mesh.py b/vispy/visuals/filters/mesh.py new file mode 100644 index 0000000000..e0b5946e16 --- /dev/null +++ b/vispy/visuals/filters/mesh.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Vispy Development Team. All Rights Reserved. +# Distributed under the (new) BSD License. See LICENSE.txt for more info. +import numpy as np +from vispy.gloo import Texture2D, VertexBuffer +from vispy.visuals.shaders import Function, Varying +from vispy.visuals.filters import Filter + + +class TextureFilter(Filter): + """Filter to apply a texture to a mesh. + + Note the texture is applied by multiplying the texture color by the + Visual's produced color. By specifying `color="white"` when creating + a `MeshVisual` the result will be the unaltered texture value. Any + other color, including the default, will result in a blending of that + color and the color of the texture. + + """ + + def __init__(self, texture, texcoords, enabled=True): + """Apply a texture on a mesh. + + Parameters + ---------- + texture : (M, N) or (M, N, C) array + The 2D texture image. + texcoords : (N, 2) array + The texture coordinates. + enabled : bool + Whether the display of the texture is enabled. + """ + vfunc = Function(""" + void pass_coords() { + $v_texcoords = $texcoords; + } + """) + ffunc = Function(""" + void apply_texture() { + if ($enabled == 1) { + gl_FragColor *= texture2D($u_texture, $texcoords); + } + } + """) + self._texcoord_varying = Varying('v_texcoord', 'vec2') + vfunc['v_texcoords'] = self._texcoord_varying + ffunc['texcoords'] = self._texcoord_varying + self._texcoords_buffer = VertexBuffer( + np.zeros((0, 2), dtype=np.float32) + ) + vfunc['texcoords'] = self._texcoords_buffer + super().__init__(vcode=vfunc, vhook='pre', fcode=ffunc) + + self.enabled = enabled + self.texture = texture + self.texcoords = texcoords + + @property + def enabled(self): + """True to display the texture, False to disable.""" + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + self.fshader['enabled'] = 1 if enabled else 0 + + @property + def texture(self): + """The texture image.""" + return self._texture + + @texture.setter + def texture(self, texture): + self._texture = texture + self.fshader['u_texture'] = Texture2D(texture) + + @property + def texcoords(self): + """The texture coordinates as an (N, 2) array of floats.""" + return self._texcoords + + @texcoords.setter + def texcoords(self, texcoords): + self._texcoords = texcoords + self._update_texcoords_buffer(texcoords) + + def _update_texcoords_buffer(self, texcoords): + if not self._attached or self._visual is None: + return + + # FIXME: Indices for texture coordinates might be different than face + # indices, although in some cases they are the same. Currently, + # vispy.io.read_mesh assumes face indices and texture coordinates are + # the same. + # TODO: + # 1. Add reading and returning of texture coordinate indices in + # read_mesh. + # 2. Add texture coordinate indices in MeshData from + # vispy.geometry.meshdata + # 3. Use mesh_data.get_texcoords_indices() here below. + tc = texcoords[self._visual.mesh_data.get_faces()] + self._texcoords_buffer.set_data(tc, convert=True) + + def _attach(self, visual): + super()._attach(visual) + self._update_texcoords_buffer(self._texcoords) diff --git a/vispy/visuals/mesh.py b/vispy/visuals/mesh.py index fe460259a9..f9ac29ec36 100644 --- a/vispy/visuals/mesh.py +++ b/vispy/visuals/mesh.py @@ -349,59 +349,35 @@ def mesh_data_changed(self): def _update_data(self): md = self.mesh_data - # Update vertex/index buffers - if self.shading == 'smooth' and not md.has_face_indexed_data(): - v = md.get_vertices() - if v is None: - return False - if v.shape[-1] == 2: - v = np.concatenate((v, np.zeros((v.shape[:-1] + (1,)))), -1) - self._vertices.set_data(v, convert=True) - self._normals.set_data(md.get_vertex_normals(), convert=True) - self._faces.set_data(md.get_faces(), convert=True) - self._index_buffer = self._faces - if md.has_vertex_color(): - colors = md.get_vertex_colors() - colors = colors.astype(np.float32) - elif md.has_face_color(): - colors = md.get_face_colors() - colors = colors.astype(np.float32) - elif md.has_vertex_value(): - colors = md.get_vertex_values()[:, np.newaxis] - colors = colors.astype(np.float32) - else: - colors = self._color.rgba + + v = md.get_vertices(indexed='faces') + if v is None: + return False + if v.shape[-1] == 2: + v = np.concatenate((v, np.zeros((v.shape[:-1] + (1,)))), -1) + self._vertices.set_data(v, convert=True) + if self.shading == 'smooth': + normals = md.get_vertex_normals(indexed='faces') + self._normals.set_data(normals, convert=True) + elif self.shading == 'flat': + normals = md.get_face_normals(indexed='faces') + self._normals.set_data(normals, convert=True) else: - # It might actually be slower to prefer the indexed='faces' mode. - # It certainly adds some complexity, and I'm not sure what the - # benefits are, or if they justify this additional complexity. - v = md.get_vertices(indexed='faces') - if v is None: - return False - if v.shape[-1] == 2: - v = np.concatenate((v, np.zeros((v.shape[:-1] + (1,)))), -1) - self._vertices.set_data(v, convert=True) - if self.shading == 'smooth': - normals = md.get_vertex_normals(indexed='faces') - self._normals.set_data(normals, convert=True) - elif self.shading == 'flat': - normals = md.get_face_normals(indexed='faces') - self._normals.set_data(normals, convert=True) - else: - self._normals.set_data(np.zeros((0, 3), dtype=np.float32)) - self._index_buffer = None - if md.has_vertex_color(): - colors = md.get_vertex_colors(indexed='faces') - colors = colors.astype(np.float32) - elif md.has_face_color(): - colors = md.get_face_colors(indexed='faces') - colors = colors.astype(np.float32) - elif md.has_vertex_value(): - colors = md.get_vertex_values(indexed='faces') - colors = colors.ravel()[:, np.newaxis] - colors = colors.astype(np.float32) - else: - colors = self._color.rgba + self._normals.set_data(np.zeros((0, 3), dtype=np.float32)) + self._index_buffer = None + if md.has_vertex_color(): + colors = md.get_vertex_colors(indexed='faces') + colors = colors.astype(np.float32) + elif md.has_face_color(): + colors = md.get_face_colors(indexed='faces') + colors = colors.astype(np.float32) + elif md.has_vertex_value(): + colors = md.get_vertex_values(indexed='faces') + colors = colors.ravel()[:, np.newaxis] + colors = colors.astype(np.float32) + else: + colors = self._color.rgba + self.shared_program.vert['position'] = self._vertices self.shared_program['texture2D_LUT'] = self._cmap.texture_lut() \