Module simple_3dviz.renderables.textured_mesh
Expand source code
from os import path
import numpy as np
from ..io import read_mesh_file, read_material_file
from ..utils import read_image
from .mesh import MeshBase
class Material(object):
"""A struct object containing information about the material.
The supported materials have the following:
- An ambient color
- A diffuse lighting color (similar to `simple_3dviz.renderables.mesh.Mesh`)
- A specular lighting color
- A specular exponent for Phong lighting
- A texture map
- A bump map
- A lighting mode from the set {'constant', 'diffuse', 'specular'}
Arguments
---------
ambient: array-like (r, g, b), float values between 0 and 1
diffuse: array-like (r, g, b), float values between 0 and 1
specular: array-like (r, g, b), float values between 0 and 1
Ns: float, the exponent used for Phong lighting
texture: array of uint8 with 3 or 4 channels and power of 2 width and
height, it contains the colors to be used by a mesh
bump_map: array of uint8 with 3 channels and power of 2 width and
height, it contains the local displacement of the normal
vectors for implementing bump mapping
"""
def __init__(self, ambient=(0.4, 0.4, 0.4), diffuse=(0.4, 0.4, 0.4),
specular=(0.1, 0.1, 0.1), Ns=2., texture=None,
bump_map=None, mode="specular"):
self.ambient = np.asarray(ambient, dtype=np.float32)
self.diffuse = np.asarray(diffuse, dtype=np.float32)
self.specular = np.asarray(specular, dtype=np.float32)
self.Ns = Ns
self.texture = texture
self.bump_map = bump_map
if mode == "constant":
self.diffuse[...] = 0
self.specular[...] = 0
elif mode == "diffuse":
self.specular[...] = 0
@classmethod
def with_texture_image(cls, texture_path, ambient=(0.4, 0.4, 0.4),
diffuse=(0.4, 0.4, 0.4), specular=(0.1, 0.1, 0.1),
Ns=2., mode="specular"):
return cls(
ambient=ambient,
diffuse=diffuse,
specular=specular,
Ns=Ns,
texture=read_image(texture_path),
mode=mode
)
class TexturedMesh(MeshBase):
"""A mesh that can use materials and textures to render a slightly more
realistic image.
Arguments
---------
vertices: array-like, the vertices of the triangles. Each triangle
should be given on its own even if vertices are shared.
normals: array-like, per vertex normal vectors
uv: array-like, per-vertex uv coordinates inside the texture
material: simple_3dviz.renderables.textured_mesh.Material object
"""
def __init__(self, vertices, normals, uv, material):
super(TexturedMesh, self).__init__(vertices, normals)
self._uv = np.asarray(uv)
assert(self._uv.shape == (len(self._vertices), 2))
self._material = material
self._texture = None
self._bump_map = None
def init(self, ctx):
self._prog = ctx.program(
vertex_shader="""
#version 330
uniform mat4 mvp;
uniform mat4 rotation;
uniform mat4 local_model;
uniform vec3 offset;
in vec3 in_vert;
in vec3 in_norm;
in vec2 in_uv;
out vec3 v_vert;
out vec3 v_norm;
out vec2 v_uv;
void main() {
vec4 t_pos = vec4(in_vert, 1.0);
vec3 t_nor = in_norm;
t_pos = local_model * t_pos;
t_pos = t_pos + vec4(offset, 0);
t_pos = mvp * t_pos;
t_nor = mat3(local_model) * t_nor;
t_nor = mat3(rotation) * t_nor;
// outputs
v_vert = t_pos.xyz / t_pos.w;
v_norm = t_nor;
v_uv = in_uv;
gl_Position = t_pos;
}
""",
fragment_shader="""
#version 330
uniform vec3 light;
uniform vec3 camera_position;
uniform vec3 ambient;
uniform vec3 diffuse;
uniform vec3 specular;
uniform float Ns;
uniform sampler2D texture;
uniform sampler2D bump_map;
uniform bool has_texture;
uniform bool has_bump_map;
in vec3 v_vert;
in vec3 v_norm;
in vec2 v_uv;
out vec4 f_color;
void main() {
// Local variables for the colors and normal
vec3 l_ambient = ambient;
vec3 l_diffuse = diffuse;
vec3 l_norm = v_norm;
// fix colors based on the textures
if (has_texture) {
vec4 texColor = texture2D(texture, v_uv);
l_ambient = l_ambient * texColor.rgb;
l_diffuse = l_diffuse * texColor.rgb;
}
// fix normal based on the bump map
if (has_bump_map) {
vec3 bump_normal = texture2D(bump_map, v_uv).rgb;
l_norm += (dot(v_norm, v_norm) - 1) * bump_normal;
}
// ambient color
f_color.rgb = l_ambient;
// diffuse color
vec3 L = normalize(light - v_vert);
f_color.rgb += l_diffuse * clamp(dot(normalize(l_norm), L), 0, 1);
// specular color
if (Ns > 0) {
vec3 V = normalize(camera_position - v_vert);
vec3 H = normalize(L + V);
f_color.rgb += specular * pow(clamp(dot(normalize(l_norm), H), -1, 1), Ns);
f_color.a = 1.0;
}
}
"""
)
self._vbo = ctx.buffer(np.hstack([
self._vertices,
self._normals,
self._uv
]).astype(np.float32).tobytes())
self._vao = ctx.simple_vertex_array(
self._prog,
self._vbo,
"in_vert", "in_norm", "in_uv"
)
self._prog["texture"] = 0
self._prog["bump_map"] = 1
self.model_matrix = self._model_matrix
self.offset = self._offset
self.material = self._material
def release(self):
super(TexturedMesh, self).release()
if self._texture is not None:
self._texture.release()
if self._bump_map is not None:
self._bump_map.release()
def _get_uniforms_list(self):
"""Return the used uniforms to fetch from the scene."""
return ["light", "camera_position", "mvp", "rotation"]
def _update_vbo(self):
"""Write in the vertex buffer object the vertices, normals and
colors."""
if self._vbo is not None:
self._vbo.write(np.hstack([
self._vertices, self._normals, self._uv, self._material
]).astype(np.float32).tobytes())
@property
def material(self):
return self._material
@material.setter
def material(self, new_material):
assert isinstance(new_material, Material)
self._material = new_material
if self._prog is None:
return
self._prog["ambient"].write(self._material.ambient.tobytes())
self._prog["diffuse"].write(self._material.diffuse.tobytes())
self._prog["specular"].write(self._material.specular.tobytes())
self._prog["Ns"] = self._material.Ns
if self._material.texture is not None:
self._prog["has_texture"] = True
if self._texture is not None:
self._texture.release()
self._texture = self._prog.ctx.texture(
self._material.texture.shape[:2][::-1],
self._material.texture.shape[2],
data=self._material.texture.tobytes()
)
else:
self._prog["has_texture"] = False
if self._material.bump_map is not None:
self._prog["has_bump_map"] = True
if self._bump_map is not None:
self._bump_map.release()
self._bump_map = self._prog.ctx.texture(
self._material.bump_map.shape[:2][::-1],
self._material.bump_map.shape[2],
data=self._material.bump_map.tobytes()
)
else:
self._prog["has_bump_map"] = False
def render(self):
if self._texture is not None:
self._texture.use(location=0)
if self._bump_map is not None:
self._bump_map.use(location=1)
super(TexturedMesh, self).render()
@classmethod
def from_file(cls, filepath, material_filepath=None, ext=None,
material_ext=None, color=(0.5, 0.5, 0.5)):
"""Read the mesh from a file.
Arguments
---------
filepath: Path to file or file object containing the mesh
material_filepath: Path to file containing the material
ext: The file extension (including the dot) if 'filepath' is an
object
material_ext: The file extension (including the dot) if 'filepath'
is an object
color: A color to use if the material is neither given nor found
"""
# Read the mesh
mesh = read_mesh_file(filepath, ext=ext)
# Extract the triangles
vertices = mesh.vertices
# Set a normal per triangle vertex
try:
normals = mesh.normals
except NotImplementedError:
normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
# Set the uv coordinates
try:
uv = mesh.uv
except NotImplementedError:
uv = np.zeros((vertices.shape[0], 2), dtype=np.float32)
# Parse the material information
mtl = None
material = Material(diffuse=color)
try:
mtl = read_material_file(mesh.material_file)
except (NotImplementedError, FileNotFoundError):
if material_filepath is not None:
mtl = read_material_file(material_filepath, ext=material_ext)
elif path.exists(path.join(path.dirname(filepath), "texture.png")):
try:
material = Material.with_texture_image(
path.join(path.dirname(filepath), "texture.png"),
diffuse=color
)
except:
import sys
print(("Error while reading the texture image "
"for {}").format(filepath), file=sys.stderr)
raise
if mtl is not None:
material = Material(
ambient=mtl.ambient,
diffuse=mtl.diffuse,
specular=mtl.specular,
Ns=mtl.Ns,
texture=mtl.texture,
bump_map=mtl.optional_bump_map
)
return cls(vertices, normals, uv, material)
@classmethod
def from_faces(cls, vertices, uv, faces, material):
vertices, uv, faces = map(np.asarray, (vertices, uv, faces))
vertices = vertices[faces].reshape(-1, 3)
uv = uv[faces].reshape(-1, 2)
normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
return cls(vertices, normals, uv, material)
Classes
class Material (ambient=(0.4, 0.4, 0.4), diffuse=(0.4, 0.4, 0.4), specular=(0.1, 0.1, 0.1), Ns=2.0, texture=None, bump_map=None, mode='specular')
-
A struct object containing information about the material.
The supported materials have the following:
- An ambient color
- A diffuse lighting color (similar to
Mesh
) - A specular lighting color
- A specular exponent for Phong lighting
- A texture map
- A bump map
- A lighting mode from the set {'constant', 'diffuse', 'specular'}
Arguments
ambient: array-like (r, g, b), float values between 0 and 1 diffuse: array-like (r, g, b), float values between 0 and 1 specular: array-like (r, g, b), float values between 0 and 1 Ns: float, the exponent used for Phong lighting texture: array of uint8 with 3 or 4 channels and power of 2 width and height, it contains the colors to be used by a mesh bump_map: array of uint8 with 3 channels and power of 2 width and height, it contains the local displacement of the normal vectors for implementing bump mapping
Expand source code
class Material(object): """A struct object containing information about the material. The supported materials have the following: - An ambient color - A diffuse lighting color (similar to `simple_3dviz.renderables.mesh.Mesh`) - A specular lighting color - A specular exponent for Phong lighting - A texture map - A bump map - A lighting mode from the set {'constant', 'diffuse', 'specular'} Arguments --------- ambient: array-like (r, g, b), float values between 0 and 1 diffuse: array-like (r, g, b), float values between 0 and 1 specular: array-like (r, g, b), float values between 0 and 1 Ns: float, the exponent used for Phong lighting texture: array of uint8 with 3 or 4 channels and power of 2 width and height, it contains the colors to be used by a mesh bump_map: array of uint8 with 3 channels and power of 2 width and height, it contains the local displacement of the normal vectors for implementing bump mapping """ def __init__(self, ambient=(0.4, 0.4, 0.4), diffuse=(0.4, 0.4, 0.4), specular=(0.1, 0.1, 0.1), Ns=2., texture=None, bump_map=None, mode="specular"): self.ambient = np.asarray(ambient, dtype=np.float32) self.diffuse = np.asarray(diffuse, dtype=np.float32) self.specular = np.asarray(specular, dtype=np.float32) self.Ns = Ns self.texture = texture self.bump_map = bump_map if mode == "constant": self.diffuse[...] = 0 self.specular[...] = 0 elif mode == "diffuse": self.specular[...] = 0 @classmethod def with_texture_image(cls, texture_path, ambient=(0.4, 0.4, 0.4), diffuse=(0.4, 0.4, 0.4), specular=(0.1, 0.1, 0.1), Ns=2., mode="specular"): return cls( ambient=ambient, diffuse=diffuse, specular=specular, Ns=Ns, texture=read_image(texture_path), mode=mode )
Static methods
def with_texture_image(texture_path, ambient=(0.4, 0.4, 0.4), diffuse=(0.4, 0.4, 0.4), specular=(0.1, 0.1, 0.1), Ns=2.0, mode='specular')
-
Expand source code
@classmethod def with_texture_image(cls, texture_path, ambient=(0.4, 0.4, 0.4), diffuse=(0.4, 0.4, 0.4), specular=(0.1, 0.1, 0.1), Ns=2., mode="specular"): return cls( ambient=ambient, diffuse=diffuse, specular=specular, Ns=Ns, texture=read_image(texture_path), mode=mode )
class TexturedMesh (vertices, normals, uv, material)
-
A mesh that can use materials and textures to render a slightly more realistic image.
Arguments
vertices: array-like, the vertices of the triangles. Each triangle should be given on its own even if vertices are shared. normals: array-like, per vertex normal vectors uv: array-like, per-vertex uv coordinates inside the texture material: simple_3dviz.renderables.textured_mesh.Material object
Expand source code
class TexturedMesh(MeshBase): """A mesh that can use materials and textures to render a slightly more realistic image. Arguments --------- vertices: array-like, the vertices of the triangles. Each triangle should be given on its own even if vertices are shared. normals: array-like, per vertex normal vectors uv: array-like, per-vertex uv coordinates inside the texture material: simple_3dviz.renderables.textured_mesh.Material object """ def __init__(self, vertices, normals, uv, material): super(TexturedMesh, self).__init__(vertices, normals) self._uv = np.asarray(uv) assert(self._uv.shape == (len(self._vertices), 2)) self._material = material self._texture = None self._bump_map = None def init(self, ctx): self._prog = ctx.program( vertex_shader=""" #version 330 uniform mat4 mvp; uniform mat4 rotation; uniform mat4 local_model; uniform vec3 offset; in vec3 in_vert; in vec3 in_norm; in vec2 in_uv; out vec3 v_vert; out vec3 v_norm; out vec2 v_uv; void main() { vec4 t_pos = vec4(in_vert, 1.0); vec3 t_nor = in_norm; t_pos = local_model * t_pos; t_pos = t_pos + vec4(offset, 0); t_pos = mvp * t_pos; t_nor = mat3(local_model) * t_nor; t_nor = mat3(rotation) * t_nor; // outputs v_vert = t_pos.xyz / t_pos.w; v_norm = t_nor; v_uv = in_uv; gl_Position = t_pos; } """, fragment_shader=""" #version 330 uniform vec3 light; uniform vec3 camera_position; uniform vec3 ambient; uniform vec3 diffuse; uniform vec3 specular; uniform float Ns; uniform sampler2D texture; uniform sampler2D bump_map; uniform bool has_texture; uniform bool has_bump_map; in vec3 v_vert; in vec3 v_norm; in vec2 v_uv; out vec4 f_color; void main() { // Local variables for the colors and normal vec3 l_ambient = ambient; vec3 l_diffuse = diffuse; vec3 l_norm = v_norm; // fix colors based on the textures if (has_texture) { vec4 texColor = texture2D(texture, v_uv); l_ambient = l_ambient * texColor.rgb; l_diffuse = l_diffuse * texColor.rgb; } // fix normal based on the bump map if (has_bump_map) { vec3 bump_normal = texture2D(bump_map, v_uv).rgb; l_norm += (dot(v_norm, v_norm) - 1) * bump_normal; } // ambient color f_color.rgb = l_ambient; // diffuse color vec3 L = normalize(light - v_vert); f_color.rgb += l_diffuse * clamp(dot(normalize(l_norm), L), 0, 1); // specular color if (Ns > 0) { vec3 V = normalize(camera_position - v_vert); vec3 H = normalize(L + V); f_color.rgb += specular * pow(clamp(dot(normalize(l_norm), H), -1, 1), Ns); f_color.a = 1.0; } } """ ) self._vbo = ctx.buffer(np.hstack([ self._vertices, self._normals, self._uv ]).astype(np.float32).tobytes()) self._vao = ctx.simple_vertex_array( self._prog, self._vbo, "in_vert", "in_norm", "in_uv" ) self._prog["texture"] = 0 self._prog["bump_map"] = 1 self.model_matrix = self._model_matrix self.offset = self._offset self.material = self._material def release(self): super(TexturedMesh, self).release() if self._texture is not None: self._texture.release() if self._bump_map is not None: self._bump_map.release() def _get_uniforms_list(self): """Return the used uniforms to fetch from the scene.""" return ["light", "camera_position", "mvp", "rotation"] def _update_vbo(self): """Write in the vertex buffer object the vertices, normals and colors.""" if self._vbo is not None: self._vbo.write(np.hstack([ self._vertices, self._normals, self._uv, self._material ]).astype(np.float32).tobytes()) @property def material(self): return self._material @material.setter def material(self, new_material): assert isinstance(new_material, Material) self._material = new_material if self._prog is None: return self._prog["ambient"].write(self._material.ambient.tobytes()) self._prog["diffuse"].write(self._material.diffuse.tobytes()) self._prog["specular"].write(self._material.specular.tobytes()) self._prog["Ns"] = self._material.Ns if self._material.texture is not None: self._prog["has_texture"] = True if self._texture is not None: self._texture.release() self._texture = self._prog.ctx.texture( self._material.texture.shape[:2][::-1], self._material.texture.shape[2], data=self._material.texture.tobytes() ) else: self._prog["has_texture"] = False if self._material.bump_map is not None: self._prog["has_bump_map"] = True if self._bump_map is not None: self._bump_map.release() self._bump_map = self._prog.ctx.texture( self._material.bump_map.shape[:2][::-1], self._material.bump_map.shape[2], data=self._material.bump_map.tobytes() ) else: self._prog["has_bump_map"] = False def render(self): if self._texture is not None: self._texture.use(location=0) if self._bump_map is not None: self._bump_map.use(location=1) super(TexturedMesh, self).render() @classmethod def from_file(cls, filepath, material_filepath=None, ext=None, material_ext=None, color=(0.5, 0.5, 0.5)): """Read the mesh from a file. Arguments --------- filepath: Path to file or file object containing the mesh material_filepath: Path to file containing the material ext: The file extension (including the dot) if 'filepath' is an object material_ext: The file extension (including the dot) if 'filepath' is an object color: A color to use if the material is neither given nor found """ # Read the mesh mesh = read_mesh_file(filepath, ext=ext) # Extract the triangles vertices = mesh.vertices # Set a normal per triangle vertex try: normals = mesh.normals except NotImplementedError: normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0) # Set the uv coordinates try: uv = mesh.uv except NotImplementedError: uv = np.zeros((vertices.shape[0], 2), dtype=np.float32) # Parse the material information mtl = None material = Material(diffuse=color) try: mtl = read_material_file(mesh.material_file) except (NotImplementedError, FileNotFoundError): if material_filepath is not None: mtl = read_material_file(material_filepath, ext=material_ext) elif path.exists(path.join(path.dirname(filepath), "texture.png")): try: material = Material.with_texture_image( path.join(path.dirname(filepath), "texture.png"), diffuse=color ) except: import sys print(("Error while reading the texture image " "for {}").format(filepath), file=sys.stderr) raise if mtl is not None: material = Material( ambient=mtl.ambient, diffuse=mtl.diffuse, specular=mtl.specular, Ns=mtl.Ns, texture=mtl.texture, bump_map=mtl.optional_bump_map ) return cls(vertices, normals, uv, material) @classmethod def from_faces(cls, vertices, uv, faces, material): vertices, uv, faces = map(np.asarray, (vertices, uv, faces)) vertices = vertices[faces].reshape(-1, 3) uv = uv[faces].reshape(-1, 2) normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0) return cls(vertices, normals, uv, material)
Ancestors
Static methods
def from_faces(vertices, uv, faces, material)
-
Expand source code
@classmethod def from_faces(cls, vertices, uv, faces, material): vertices, uv, faces = map(np.asarray, (vertices, uv, faces)) vertices = vertices[faces].reshape(-1, 3) uv = uv[faces].reshape(-1, 2) normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0) return cls(vertices, normals, uv, material)
def from_file(filepath, material_filepath=None, ext=None, material_ext=None, color=(0.5, 0.5, 0.5))
-
Read the mesh from a file.
Arguments
filepath: Path to file or file object containing the mesh material_filepath: Path to file containing the material ext: The file extension (including the dot) if 'filepath' is an object material_ext: The file extension (including the dot) if 'filepath' is an object color: A color to use if the material is neither given nor found
Expand source code
@classmethod def from_file(cls, filepath, material_filepath=None, ext=None, material_ext=None, color=(0.5, 0.5, 0.5)): """Read the mesh from a file. Arguments --------- filepath: Path to file or file object containing the mesh material_filepath: Path to file containing the material ext: The file extension (including the dot) if 'filepath' is an object material_ext: The file extension (including the dot) if 'filepath' is an object color: A color to use if the material is neither given nor found """ # Read the mesh mesh = read_mesh_file(filepath, ext=ext) # Extract the triangles vertices = mesh.vertices # Set a normal per triangle vertex try: normals = mesh.normals except NotImplementedError: normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0) # Set the uv coordinates try: uv = mesh.uv except NotImplementedError: uv = np.zeros((vertices.shape[0], 2), dtype=np.float32) # Parse the material information mtl = None material = Material(diffuse=color) try: mtl = read_material_file(mesh.material_file) except (NotImplementedError, FileNotFoundError): if material_filepath is not None: mtl = read_material_file(material_filepath, ext=material_ext) elif path.exists(path.join(path.dirname(filepath), "texture.png")): try: material = Material.with_texture_image( path.join(path.dirname(filepath), "texture.png"), diffuse=color ) except: import sys print(("Error while reading the texture image " "for {}").format(filepath), file=sys.stderr) raise if mtl is not None: material = Material( ambient=mtl.ambient, diffuse=mtl.diffuse, specular=mtl.specular, Ns=mtl.Ns, texture=mtl.texture, bump_map=mtl.optional_bump_map ) return cls(vertices, normals, uv, material)
Instance variables
var material
-
Expand source code
@property def material(self): return self._material
Inherited members