Skip to content

Commit

Permalink
Merge pull request #1444 from asnt/mesh-texture
Browse files Browse the repository at this point in the history
Add TextureFilter for adding textures to MeshVisuals
  • Loading branch information
djhoese committed May 4, 2020
2 parents 326f6f3 + ec2f03b commit f1de04c
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 56 deletions.
62 changes: 62 additions & 0 deletions 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()
2 changes: 2 additions & 0 deletions vispy/visuals/filters/__init__.py
Expand Up @@ -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
16 changes: 12 additions & 4 deletions vispy/visuals/filters/base_filter.py
Expand Up @@ -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'}
Expand All @@ -61,21 +61,23 @@ 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
else:
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.
Expand All @@ -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.
Expand All @@ -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
107 changes: 107 additions & 0 deletions 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)
80 changes: 28 additions & 52 deletions vispy/visuals/mesh.py
Expand Up @@ -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() \
Expand Down

0 comments on commit f1de04c

Please sign in to comment.