# ---------------------------------------------------------------------------- # pyglet # Copyright (c) 2006-2008 Alex Holkner # Copyright (c) 2008-2021 pyglet contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of pyglet nor the names of its # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- """Loading of 3D models. A :py:class:`~pyglet.model.Model` is an instance of a 3D object. The following example loads a ``"teapot.obj"`` model:: import pyglet window = pyglet.window.Window() teapot = pyglet.model.load('teapot.obj') @window.event def on_draw(): teapot.draw() pyglet.app.run() You can also load models with :py:meth:`~pyglet.resource.model`. See :py:mod:`~pyglet.resource` for more information. Efficient Drawing ================= As with Sprites or Text, Models can be added to a :py:class:`~pyglet.graphics.Batch` for efficient drawing. This is preferred to calling their ``draw`` methods individually. To do this, simply pass in a reference to the :py:class:`~pyglet.graphics.Batch` instance when loading the Model:: import pyglet window = pyglet.window.Window() batch = pyglet.graphics.Batch() teapot = pyglet.model.load('teapot.obj', batch=batch) @window.event def on_draw(): batch.draw() pyglet.app.run() .. versionadded:: 1.4 """ from math import radians from io import BytesIO import pyglet from pyglet import gl from pyglet import graphics from pyglet.gl import current_context from pyglet.math import Mat4, Vec3 from pyglet.graphics import shader from .codecs import ModelDecodeException from .codecs import add_encoders, add_decoders, add_default_model_codecs from .codecs import get_encoders, get_decoders def load(filename, file=None, decoder=None, batch=None): """Load a 3D model from a file. :Parameters: `filename` : str Used to guess the model format, and to load the file if `file` is unspecified. `file` : file-like object or None Source of model data in any supported format. `decoder` : ModelDecoder or None If unspecified, all decoders that are registered for the filename extension are tried. An exception is raised if no codecs are registered for the file extension, or if decoding fails. `batch` : Batch or None An optional Batch instance to add this model to. :rtype: :py:mod:`~pyglet.model.Model` """ if not file: file = open(filename, 'rb') if not hasattr(file, 'seek'): file = BytesIO(file.read()) try: if decoder: return decoder.decode(file, filename, batch) else: first_exception = None for decoder in get_decoders(filename): try: model = decoder.decode(file, filename, batch) return model except ModelDecodeException as e: if (not first_exception or first_exception.exception_priority < e.exception_priority): first_exception = e file.seek(0) if not first_exception: raise ModelDecodeException('No decoders are available for this model format.') raise first_exception finally: file.close() def get_default_shader(): try: return pyglet.gl.current_context.model_default_plain_shader except AttributeError: vert_shader = shader.Shader(MaterialGroup.default_vert_src, 'vertex') frag_shader = shader.Shader(MaterialGroup.default_frag_src, 'fragment') default_shader_program = shader.ShaderProgram(vert_shader, frag_shader) pyglet.gl.current_context.model_default_plain_shader = default_shader_program return pyglet.gl.current_context.model_default_plain_shader def get_default_textured_shader(): try: return pyglet.gl.current_context.model_default_textured_shader except AttributeError: vert_shader = shader.Shader(TexturedMaterialGroup.default_vert_src, 'vertex') frag_shader = shader.Shader(TexturedMaterialGroup.default_frag_src, 'fragment') default_shader_program = shader.ShaderProgram(vert_shader, frag_shader) pyglet.gl.current_context.model_default_textured_shader = default_shader_program return current_context.model_default_textured_shader class Model: """Instance of a 3D object. See the module documentation for usage. """ def __init__(self, vertex_lists, groups, batch): """Create a model. :Parameters: `vertex_lists` : list A list of `~pyglet.graphics.VertexList` or `~pyglet.graphics.IndexedVertexList`. `groups` : list A list of `~pyglet.model.TexturedMaterialGroup`, or `~pyglet.model.MaterialGroup`. Each group corresponds to a vertex list in `vertex_lists` of the same index. `batch` : `~pyglet.graphics.Batch` Optional batch to add the model to. If no batch is provided, the model will maintain its own internal batch. """ self.vertex_lists = vertex_lists self.groups = groups self._batch = batch self._rotation = Vec3() self._translation = Vec3() @property def batch(self): """The graphics Batch that the Model belongs to. The Model can be migrated from one batch to another, or removed from a batch (for individual drawing). If not part of any batch, the Model will keep its own internal batch. Note that batch migration can be an expensive operation. :type: :py:class:`pyglet.graphics.Batch` """ return self._batch @batch.setter def batch(self, batch): if self._batch == batch: return if batch is None: batch = graphics.Batch() for group, vlist in zip(self.groups, self.vertex_lists): self._batch.migrate(vlist, gl.GL_TRIANGLES, group, batch) self._batch = batch @property def rotation(self): return self._rotation @rotation.setter def rotation(self, values): self._rotation = values for group in self.groups: group.rotation = values @property def translation(self): return self._translation @translation.setter def translation(self, values): self._translation = values for group in self.groups: group.translation = values def draw(self): """Draw the model. This is not recommended. See the module documentation for information on efficient drawing of multiple models. """ gl.current_context.window_block.bind(0) self._batch.draw_subset(self.vertex_lists) class Material: __slots__ = ("name", "diffuse", "ambient", "specular", "emission", "shininess", "texture_name") def __init__(self, name, diffuse, ambient, specular, emission, shininess, texture_name=None): self.name = name self.diffuse = diffuse self.ambient = ambient self.specular = specular self.emission = emission self.shininess = shininess self.texture_name = texture_name def __eq__(self, other): return (self.name == other.name and self.diffuse == other.diffuse and self.ambient == other.ambient and self.specular == other.specular and self.emission == other.emission and self.shininess == other.shininess and self.texture_name == other.texture_name) class BaseMaterialGroup(graphics.ShaderGroup): default_vert_src = None default_frag_src = None def __init__(self, material, program, order=0, parent=None): super().__init__(program, order, parent) self.material = material self.rotation = Vec3() self.translation = Vec3() def set_modelview_matrix(self): # NOTE: Matrix operations can be optimized later with transform feedback view = Mat4() view = view.rotate(radians(self.rotation[2]), Vec3(0, 0, 1)) view = view.rotate(radians(self.rotation[1]), Vec3(0, 1, 0)) view = view.rotate(radians(self.rotation[0]), Vec3(1, 0, 0)) view = view.translate(self.translation) # TODO: separate the projection block, and remove this hack block = self.program.uniform_blocks['WindowBlock'] ubo = block.create_ubo(0) with ubo as window_block: window_block.projection[:] = pyglet.math.Mat4.perspective_projection(0, 720, 0, 480, z_near=0.1, z_far=255) window_block.view[:] = view class TexturedMaterialGroup(BaseMaterialGroup): default_vert_src = """#version 330 core in vec3 vertices; in vec3 normals; in vec2 tex_coords; in vec4 colors; out vec4 vertex_colors; out vec3 vertex_normals; out vec2 texture_coords; out vec3 vertex_position; uniform WindowBlock { mat4 projection; mat4 view; } window; void main() { vec4 pos = window.view * vec4(vertices, 1.0); gl_Position = window.projection * pos; mat3 normal_matrix = transpose(inverse(mat3(window.view))); vertex_position = pos.xyz; vertex_colors = colors; texture_coords = tex_coords; vertex_normals = normal_matrix * normals; } """ default_frag_src = """#version 330 core in vec4 vertex_colors; in vec3 vertex_normals; in vec2 texture_coords; in vec3 vertex_position; out vec4 final_colors; uniform sampler2D our_texture; void main() { float l = dot(normalize(-vertex_position), normalize(vertex_normals)); final_colors = (texture(our_texture, texture_coords) * vertex_colors) * l * 1.2; } """ def __init__(self, material, texture): super().__init__(material, get_default_textured_shader()) self.texture = texture def set_state(self): gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(self.texture.target, self.texture.id) self.program.use() self.set_modelview_matrix() def unset_state(self): gl.glBindTexture(self.texture.target, 0) def __hash__(self): return hash((self.texture.target, self.texture.id, self.program, self.order, self.parent)) def __eq__(self, other): return (self.__class__ is other.__class__ and self.material == other.material and self.texture.target == other.texture.target and self.texture.id == other.texture.id and self.program == other.program and self.order == other.order and self.parent == other.parent) class MaterialGroup(BaseMaterialGroup): default_vert_src = """#version 330 core in vec3 vertices; in vec3 normals; in vec4 colors; out vec4 vertex_colors; out vec3 vertex_normals; out vec3 vertex_position; uniform WindowBlock { mat4 projection; mat4 view; } window; void main() { vec4 pos = window.view * vec4(vertices, 1.0); gl_Position = window.projection * pos; mat3 normal_matrix = transpose(inverse(mat3(window.view))); vertex_position = pos.xyz; vertex_colors = colors; vertex_normals = normal_matrix * normals; } """ default_frag_src = """#version 330 core in vec4 vertex_colors; in vec3 vertex_normals; in vec3 vertex_position; out vec4 final_colors; void main() { float l = dot(normalize(-vertex_position), normalize(vertex_normals)); final_colors = vertex_colors * l * 1.2; } """ def __init__(self, material): super().__init__(material, get_default_shader()) def set_state(self): self.program.use() self.set_modelview_matrix() add_default_model_codecs()