├── py3d ├── core │ ├── __init__.py │ ├── input.py │ ├── attribute.py │ ├── base.py │ ├── utils.py │ └── matrix.py ├── extras │ ├── __init__.py │ ├── directional_light.py │ ├── point_light.py │ ├── axes.py │ ├── grid.py │ ├── text_texture.py │ ├── postprocessor.py │ └── movement_rig.py ├── light │ ├── __init__.py │ ├── ambient.py │ ├── directional.py │ ├── point.py │ ├── light.py │ └── shadow.py ├── core_ext │ ├── __init__.py │ ├── scene.py │ ├── group.py │ ├── camera.py │ ├── mesh.py │ ├── render_target.py │ └── texture.py ├── effects │ ├── __init__.py │ ├── template.py │ ├── invert.py │ ├── color_reduce.py │ ├── tint.py │ ├── bright_filter.py │ ├── pixelate.py │ ├── additive_blend.py │ ├── vertical_blur.py │ ├── horizontal_blur.py │ └── vignette.py ├── geometry │ ├── __init__.py │ ├── sphere.py │ ├── prism.py │ ├── pyramid.py │ ├── cone.py │ ├── cylinder.py │ ├── plane.py │ ├── ellipsoid.py │ ├── rectangle.py │ ├── box.py │ ├── cylindrical.py │ ├── polygon.py │ └── geometry.py ├── material │ ├── __init__.py │ ├── point.py │ ├── depth.py │ ├── line.py │ ├── surface.py │ ├── lighted.py │ ├── basic.py │ ├── texture.py │ ├── material.py │ └── sprite.py ├── images │ ├── sky.jpg │ ├── crate.jpg │ ├── grass.jpg │ ├── grid.jpg │ ├── brick-wall.jpg │ ├── rgb-noise.jpg │ ├── crate-simulator.png │ ├── sonic-spritesheet.jpg │ └── brick-wall-normal-map.jpg └── __init__.py ├── .editorconfig ├── examples ├── 2-01--empty-window.py ├── 2-10--keys-down-up-press.py ├── 5-01--textured-square.py ├── 4-6--axes-and-xy-grid.py ├── 2-02--single-point.py ├── 4-1--spinning-cube.py ├── 4-2--custom-geometry.py ├── 5-07--text-texture.py ├── 4-7--box-on-grid.py ├── template.py ├── 4-4--sphere.py ├── 5-02--textured-skysphere-and-grass.py ├── 2-03--hexagon.py ├── 5-09--billboarding-by-spritesheet.py ├── 4-3--sine.py ├── 5-12-3--postprocessing-pixelation.py ├── 5-12-2--postprocessing-color-inversion.py ├── 4-5--rippling-effect.py ├── 5-12-4--postprocessing-color-reducing.py ├── 6-3-1--postprocessing-bright-filter.py ├── 5-12-1--postprocessing-color-tinting.py ├── 5-12-5--postprocessing-vignette.py ├── 6-2--bump-mapping.py ├── 2-05--hexagon-with-vertex-colors.py ├── 6-3-2--postprocessing-blur.py ├── 5-03--rippling-effect-in-texture.py ├── 2-04--shapes.py ├── 2-08--animation-2.py ├── 2-06--two-triangles.py ├── 5-08--billboarding-by-look-at.py ├── 2-09--color-changing.py ├── 5-12-6--postprocessing-compound-effect.py ├── 2-07--animation-1.py ├── 6-3-3--postprocessing-additive-blend.py ├── 5-05--distorting-texture.py ├── 5-04--blending-between-textures.py ├── 2-11--key-control.py ├── 6-3-4--postprocessor-light-bloom.py ├── 5-10--heads-up-display.py ├── 5-11--render-targets.py ├── 6-5--shadows.py ├── 5-06-3--procedural-textures-lava.py ├── 5-06-2--procedural-textures-clouds.py ├── 5-06-4--procedural-textures-marble.py ├── 5-06-1--procedural-textures-smoothed-random.py ├── 6-4--postprocessing-glow.py ├── 5-06-5--procedural-textures-wood-grain.py ├── 6-1-2--dynamical-lighting.py └── 6-1-1--lighted-spheres.py └── README.md /py3d/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py3d/extras/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py3d/light/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py3d/core_ext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py3d/effects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py3d/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /py3d/material/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /py3d/images/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/sky.jpg -------------------------------------------------------------------------------- /py3d/images/crate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/crate.jpg -------------------------------------------------------------------------------- /py3d/images/grass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/grass.jpg -------------------------------------------------------------------------------- /py3d/images/grid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/grid.jpg -------------------------------------------------------------------------------- /py3d/images/brick-wall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/brick-wall.jpg -------------------------------------------------------------------------------- /py3d/images/rgb-noise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/rgb-noise.jpg -------------------------------------------------------------------------------- /py3d/images/crate-simulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/crate-simulator.png -------------------------------------------------------------------------------- /py3d/images/sonic-spritesheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/sonic-spritesheet.jpg -------------------------------------------------------------------------------- /py3d/images/brick-wall-normal-map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax-va/PyOpenGL-Pygame-Stemkoski-Pascale-2021/HEAD/py3d/images/brick-wall-normal-map.jpg -------------------------------------------------------------------------------- /py3d/light/ambient.py: -------------------------------------------------------------------------------- 1 | from py3d.light.light import Light 2 | 3 | 4 | class AmbientLight(Light): 5 | def __init__(self, color=(1, 1, 1)): 6 | super().__init__(Light.AMBIENT) 7 | self._color = color 8 | -------------------------------------------------------------------------------- /py3d/core_ext/scene.py: -------------------------------------------------------------------------------- 1 | from py3d.core_ext.object3d import Object3D 2 | 3 | 4 | class Scene(Object3D): 5 | """ Represents the root node of the tree """ 6 | 7 | def __init__(self): 8 | super().__init__() 9 | -------------------------------------------------------------------------------- /py3d/light/directional.py: -------------------------------------------------------------------------------- 1 | from py3d.light.light import Light 2 | 3 | 4 | class DirectionalLight(Light): 5 | def __init__(self, color=(1, 1, 1), direction=(0, -1, 0)): 6 | super().__init__(Light.DIRECTIONAL) 7 | self._color = color 8 | self.set_direction(direction) 9 | -------------------------------------------------------------------------------- /py3d/geometry/sphere.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.ellipsoid import EllipsoidGeometry 2 | 3 | 4 | class SphereGeometry(EllipsoidGeometry): 5 | def __init__(self, radius=1, theta_segments=16, phi_segments=32): 6 | super().__init__(2*radius, 2*radius, 2*radius, theta_segments, phi_segments) 7 | -------------------------------------------------------------------------------- /py3d/geometry/prism.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.cylindrical import CylindricalGeometry 2 | 3 | 4 | class PrismGeometry(CylindricalGeometry): 5 | def __init__(self, radius=1, height=1, sides=6, height_segments=4, closed=True): 6 | super().__init__(radius, radius, height, sides, height_segments, closed, closed) -------------------------------------------------------------------------------- /py3d/geometry/pyramid.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.cylindrical import CylindricalGeometry 2 | 3 | 4 | class PyramidGeometry(CylindricalGeometry): 5 | def __init__(self, radius=1, height=1, sides=4, height_segments=4, closed=True): 6 | super().__init__(0, radius, height, sides, height_segments, False, closed) -------------------------------------------------------------------------------- /py3d/core_ext/group.py: -------------------------------------------------------------------------------- 1 | from py3d.core_ext.object3d import Object3D 2 | 3 | 4 | class Group(Object3D): 5 | """ 6 | Represents an interior node to which other nodes are attached 7 | to more easily transform them as a single unit 8 | """ 9 | def __init__(self): 10 | super().__init__() -------------------------------------------------------------------------------- /py3d/geometry/cone.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.cylindrical import CylindricalGeometry 2 | 3 | 4 | class ConeGeometry(CylindricalGeometry): 5 | def __init__(self, radius=1, height=1, radial_segments=32, height_segments=16, closed=True): 6 | super().__init__(0, radius, height, radial_segments, height_segments, False, closed) 7 | -------------------------------------------------------------------------------- /py3d/geometry/cylinder.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.cylindrical import CylindricalGeometry 2 | 3 | 4 | class CylinderGeometry(CylindricalGeometry): 5 | def __init__(self, radius=1, height=1, radial_segments=32, height_segments=1, closed=True): 6 | super().__init__(radius, radius, height, radial_segments, height_segments, closed, closed) 7 | -------------------------------------------------------------------------------- /examples/2-01--empty-window.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | 4 | 5 | class Example(Base): 6 | """ Render a window """ 7 | def initialize(self): 8 | print("Initializing program...") 9 | 10 | def update(self): 11 | pass 12 | 13 | 14 | # Instantiate this class and run the program 15 | Example().run() -------------------------------------------------------------------------------- /py3d/geometry/plane.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.parametric import ParametricGeometry 2 | 3 | 4 | class PlaneGeometry(ParametricGeometry): 5 | def __init__(self, width=1, height=1, width_segments=8, height_segments=8): 6 | super().__init__(-width / 2, width / 2, width_segments, 7 | -height / 2, height / 2, height_segments, 8 | lambda u, v: [u, v, 0]) 9 | -------------------------------------------------------------------------------- /py3d/light/point.py: -------------------------------------------------------------------------------- 1 | from py3d.light.light import Light 2 | 3 | 4 | class PointLight(Light): 5 | def __init__(self, 6 | color=(1, 1, 1), 7 | position=(0, 0, 0), 8 | attenuation=(1, 0, 0.1) 9 | ): 10 | super().__init__(Light.POINT) 11 | self._color = color 12 | self._attenuation = attenuation 13 | self.set_position(position) 14 | -------------------------------------------------------------------------------- /py3d/__init__.py: -------------------------------------------------------------------------------- 1 | __dependencies__ = { 2 | 'necessary': 3 | { 4 | 'pygame': '2.0.1', 5 | 'PyOpenGL': '3.1.5', 6 | 'PyOpenGL-accelerate': '3.1.5', 7 | 'numpy': '1.21.1', 8 | }, 9 | } 10 | __doc__ = """ 11 | OpenGL examples with PyOpenGL and Pygame based on the book "Developing Graphics Frameworks 12 | with Python and OpenGL" by Lee Stemkoski and Michael Pascale published by CRC Press, 2022 13 | """ 14 | -------------------------------------------------------------------------------- /py3d/light/light.py: -------------------------------------------------------------------------------- 1 | from py3d.core_ext.object3d import Object3D 2 | 3 | 4 | class Light(Object3D): 5 | AMBIENT = 1 6 | DIRECTIONAL = 2 7 | POINT = 3 8 | 9 | def __init__(self, light_type=0): 10 | super().__init__() 11 | self._light_type = light_type 12 | self._color = [1, 1, 1] 13 | self._attenuation = [1, 0, 0] 14 | 15 | @property 16 | def light_type(self): 17 | return self._light_type 18 | 19 | @property 20 | def color(self): 21 | return self._color 22 | 23 | @property 24 | def attenuation(self): 25 | return self._attenuation 26 | -------------------------------------------------------------------------------- /py3d/extras/directional_light.py: -------------------------------------------------------------------------------- 1 | from py3d.extras.grid import GridHelper 2 | 3 | 4 | class DirectionalLightHelper(GridHelper): 5 | def __init__(self, directional_light): 6 | color = directional_light.color 7 | super().__init__( 8 | size=1, 9 | divisions=6, 10 | grid_color=color, 11 | center_color=color 12 | ) 13 | self.geometry.attribute_dict["vertexPosition"].data.extend([[0, 0, 0], [0, 0, -10]]) 14 | self.geometry.attribute_dict["vertexColor"].data.extend([color, color]) 15 | self.geometry.upload_data(["vertexPosition", "vertexColor"]) 16 | -------------------------------------------------------------------------------- /examples/2-10--keys-down-up-press.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | 4 | 5 | # Check input 6 | class Example(Base): 7 | """ Create a text-based application to verify that the key press works as expected """ 8 | def initialize(self): 9 | print("Initializing program...") 10 | 11 | def update(self): 12 | if self.input.is_key_down('space'): 13 | print("The 'space' key was just pressed down") 14 | if self.input.is_key_up('space'): 15 | print("The 'space' key was just pressed up") 16 | if self.input.is_key_pressed('right'): 17 | print("The 'right' key is currently being pressed") 18 | 19 | 20 | # Instantiate this class and run the program 21 | Example().run() 22 | -------------------------------------------------------------------------------- /py3d/extras/point_light.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.sphere import SphereGeometry 2 | from py3d.material.surface import SurfaceMaterial 3 | from py3d.core_ext.mesh import Mesh 4 | 5 | 6 | class PointLightHelper(Mesh): 7 | def __init__(self, point_light, size=0.1, line_width=1): 8 | color = point_light.color 9 | geometry = SphereGeometry( 10 | radius=size, 11 | theta_segments=2, 12 | phi_segments=4) 13 | material = SurfaceMaterial( 14 | property_dict={ 15 | "baseColor": color, 16 | "wireframe": True, 17 | "doubleSide": True, 18 | "lineWidth": line_width, 19 | } 20 | ) 21 | super().__init__(geometry, material) 22 | -------------------------------------------------------------------------------- /py3d/material/point.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.material.basic import BasicMaterial 4 | 5 | 6 | class PointMaterial(BasicMaterial): 7 | def __init__(self, vertex_shader_code=None, fragment_shader_code=None, property_dict=None, use_vertex_colors=True): 8 | super().__init__(vertex_shader_code, fragment_shader_code, use_vertex_colors) 9 | # Render vertices as points 10 | self._setting_dict["drawStyle"] = GL.GL_POINTS 11 | # Set the width and height of points, in pixels 12 | self._setting_dict["pointSize"] = 8 13 | # Draw points as rounded 14 | self._setting_dict["roundedPoints"] = False 15 | self.set_properties(property_dict) 16 | 17 | def update_render_settings(self): 18 | GL.glPointSize(self._setting_dict["pointSize"]) 19 | if self._setting_dict["roundedPoints"]: 20 | GL.glEnable(GL.GL_POINT_SMOOTH) 21 | else: 22 | GL.glDisable(GL.GL_POINT_SMOOTH) 23 | -------------------------------------------------------------------------------- /py3d/material/depth.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class DepthMaterial(Material): 5 | 6 | def __init__(self): 7 | # vertex shader code 8 | vertex_shader_code = """ 9 | in vec3 vertexPosition; 10 | uniform mat4 projectionMatrix; 11 | uniform mat4 viewMatrix; 12 | uniform mat4 modelMatrix; 13 | 14 | void main() 15 | { 16 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1); 17 | } 18 | """ 19 | 20 | # fragment shader code 21 | fragment_shader_code = """ 22 | out vec4 fragColor; 23 | 24 | void main() 25 | { 26 | float z = gl_FragCoord.z; 27 | fragColor = vec4(z, z, z, 1); 28 | } 29 | """ 30 | 31 | # Initialize shaders 32 | super().__init__(vertex_shader_code, fragment_shader_code) 33 | self.locate_uniforms() 34 | -------------------------------------------------------------------------------- /py3d/effects/template.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class TemplateEffect(Material): 5 | def __init__(self): 6 | # pass-through shader 7 | vertex_shader_code = """ 8 | in vec2 vertexPosition; 9 | in vec2 vertexUV; 10 | out vec2 UV; 11 | 12 | void main() 13 | { 14 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 15 | UV = vertexUV; 16 | } 17 | """ 18 | # pass-through shader 19 | fragment_shader_code = """ 20 | in vec2 UV; 21 | uniform sampler2D textureSampler; 22 | out vec4 fragColor; 23 | 24 | void main() 25 | { 26 | vec4 color = texture(textureSampler, UV); 27 | fragColor = color; 28 | } 29 | """ 30 | super().__init__(vertex_shader_code, fragment_shader_code) 31 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 32 | self.locate_uniforms() 33 | -------------------------------------------------------------------------------- /py3d/effects/invert.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class InvertEffect(Material): 5 | """ 6 | Invert colors 7 | """ 8 | def __init__(self): 9 | vertex_shader_code = """ 10 | in vec2 vertexPosition; 11 | in vec2 vertexUV; 12 | out vec2 UV; 13 | 14 | void main() 15 | { 16 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 17 | UV = vertexUV; 18 | } 19 | """ 20 | fragment_shader_code = """ 21 | in vec2 UV; 22 | uniform sampler2D textureSampler; 23 | out vec4 fragColor; 24 | 25 | void main() 26 | { 27 | vec4 color = texture(textureSampler, UV); 28 | vec4 invert = vec4(1 - color.r, 1 - color.g, 1 - color.b, 1); 29 | fragColor = invert; 30 | } 31 | """ 32 | super().__init__(vertex_shader_code, fragment_shader_code) 33 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 34 | self.locate_uniforms() 35 | -------------------------------------------------------------------------------- /py3d/extras/axes.py: -------------------------------------------------------------------------------- 1 | from py3d.core_ext.mesh import Mesh 2 | from py3d.geometry.geometry import Geometry 3 | from py3d.material.line import LineMaterial 4 | 5 | 6 | class AxesHelper(Mesh): 7 | def __init__(self, axis_length=1, line_width=4, axis_colors=([1, 0, 0], [0, 1, 0], [0, 0, 1])): 8 | geomerty = Geometry() 9 | position_data = [[0, 0, 0], [axis_length, 0, 0], 10 | [0, 0, 0], [0, axis_length, 0], 11 | [0, 0, 0], [0, 0, axis_length]] 12 | color_data = [axis_colors[0], axis_colors[0], 13 | axis_colors[1], axis_colors[1], 14 | axis_colors[2], axis_colors[2]] 15 | geomerty.add_attribute("vec3", "vertexPosition", position_data) 16 | geomerty.add_attribute("vec3", "vertexColor", color_data) 17 | material = LineMaterial( 18 | property_dict = { 19 | "useVertexColors": True, 20 | "lineWidth": line_width, 21 | "lineType": "segments" 22 | } 23 | ) 24 | # Initialize the mesh 25 | super().__init__(geomerty, material) -------------------------------------------------------------------------------- /py3d/effects/color_reduce.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class ColorReduceEffect(Material): 5 | """ 6 | Reduce colors 7 | """ 8 | def __init__(self, levels=5): 9 | vertex_shader_code = """ 10 | in vec2 vertexPosition; 11 | in vec2 vertexUV; 12 | out vec2 UV; 13 | 14 | void main() 15 | { 16 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 17 | UV = vertexUV; 18 | } 19 | """ 20 | fragment_shader_code = """ 21 | in vec2 UV; 22 | uniform sampler2D textureSampler; 23 | uniform float levels; 24 | out vec4 fragColor; 25 | 26 | void main() 27 | { 28 | vec4 color = texture(textureSampler, UV); 29 | vec4 reduced = round(color * levels) / levels; 30 | reduced.a = 1.0; 31 | fragColor = reduced; 32 | } 33 | """ 34 | super().__init__(vertex_shader_code, fragment_shader_code) 35 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 36 | self.add_uniform("float", "levels", levels) 37 | self.locate_uniforms() 38 | -------------------------------------------------------------------------------- /py3d/effects/tint.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class TintEffect(Material): 5 | """ 6 | Tint colors 7 | """ 8 | def __init__(self, tint_color=(1, 0, 0)): 9 | vertex_shader_code = """ 10 | in vec2 vertexPosition; 11 | in vec2 vertexUV; 12 | out vec2 UV; 13 | 14 | void main() 15 | { 16 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 17 | UV = vertexUV; 18 | } 19 | """ 20 | fragment_shader_code = """ 21 | in vec2 UV; 22 | uniform vec3 tintColor; 23 | uniform sampler2D textureSampler; 24 | out vec4 fragColor; 25 | 26 | void main() 27 | { 28 | vec4 color = texture(textureSampler, UV); 29 | float gray = (color.r + color.g + color.b) / 3.0; 30 | fragColor = vec4(gray * tintColor, 1.0); 31 | } 32 | """ 33 | super().__init__(vertex_shader_code, fragment_shader_code) 34 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 35 | self.add_uniform("vec3", "tintColor", tint_color) 36 | self.locate_uniforms() 37 | -------------------------------------------------------------------------------- /py3d/core_ext/camera.py: -------------------------------------------------------------------------------- 1 | from numpy.linalg import inv 2 | 3 | from py3d.core.matrix import Matrix 4 | from py3d.core_ext.object3d import Object3D 5 | 6 | 7 | class Camera(Object3D): 8 | """ Represents the virtual camera used to view the scene """ 9 | def __init__(self, angle_of_view=60, aspect_ratio=1, near=0.1, far=1000): 10 | super().__init__() 11 | self._projection_matrix = Matrix.make_perspective(angle_of_view, aspect_ratio, near, far) 12 | self._view_matrix = Matrix.make_identity() # inverse of self._matrix 13 | 14 | @property 15 | def projection_matrix(self): 16 | return self._projection_matrix 17 | 18 | @property 19 | def view_matrix(self): 20 | return self._view_matrix 21 | 22 | def set_perspective(self, angle_of_view=50, aspect_ratio=1, near=0.1, far=1000): 23 | self._projection_matrix = Matrix.make_perspective(angle_of_view, aspect_ratio, near, far) 24 | 25 | def set_orthographic(self, left=-1, right=1, bottom=-1, top=1, near=-1, far=1): 26 | self._projection_matrix = Matrix.make_orthographic(left, right, bottom, top, near, far) 27 | 28 | def update_view_matrix(self): 29 | self._view_matrix = inv(self.global_matrix) -------------------------------------------------------------------------------- /py3d/effects/bright_filter.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class BrightFilterEffect(Material): 5 | def __init__(self, threshold=2.0): 6 | # pass-through shader 7 | vertex_shader_code = """ 8 | in vec2 vertexPosition; 9 | in vec2 vertexUV; 10 | out vec2 UV; 11 | 12 | void main() 13 | { 14 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 15 | UV = vertexUV; 16 | } 17 | """ 18 | # pass-through shader 19 | fragment_shader_code = """ 20 | in vec2 UV; 21 | uniform sampler2D textureSampler; 22 | uniform float threshold; 23 | out vec4 fragColor; 24 | 25 | void main() 26 | { 27 | vec4 color = texture(textureSampler, UV); 28 | if (color.r + color.g + color.b < threshold) 29 | discard; 30 | fragColor = color; 31 | } 32 | """ 33 | super().__init__(vertex_shader_code, fragment_shader_code) 34 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 35 | self.add_uniform("float", "threshold", threshold) 36 | self.locate_uniforms() 37 | -------------------------------------------------------------------------------- /examples/5-01--textured-square.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.texture import Texture 8 | from py3d.geometry.rectangle import RectangleGeometry 9 | from py3d.material.texture import TextureMaterial 10 | 11 | 12 | class Example(Base): 13 | """ Render a textured square """ 14 | def initialize(self): 15 | print("Initializing program...") 16 | self.renderer = Renderer() 17 | self.scene = Scene() 18 | self.camera = Camera(aspect_ratio=800/600) 19 | self.camera.set_position([0, 0, 2]) 20 | geometry = RectangleGeometry() 21 | grid_texture = Texture(file_name="../py3d/images/grid.jpg") 22 | material = TextureMaterial(texture=grid_texture) 23 | self.mesh = Mesh( 24 | geometry=geometry, 25 | material=material 26 | ) 27 | self.scene.add(self.mesh) 28 | 29 | def update(self): 30 | self.renderer.render(self.scene, self.camera) 31 | 32 | 33 | # Instantiate this class and run the program 34 | Example(screen_size=[800, 600]).run() 35 | -------------------------------------------------------------------------------- /py3d/material/line.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.material.basic import BasicMaterial 4 | 5 | 6 | class LineMaterial(BasicMaterial): 7 | def __init__(self, vertex_shader_code=None, fragment_shader_code=None, property_dict=None, use_vertex_colors=True): 8 | super().__init__(vertex_shader_code, fragment_shader_code, use_vertex_colors) 9 | # Render vertices as continuous line by default 10 | self._setting_dict["drawStyle"] = GL.GL_LINE_STRIP 11 | # Set the line thickness 12 | self._setting_dict["lineWidth"] = 1 13 | # line type: "connected" | "loop" | "segments" 14 | self._setting_dict["lineType"] = "connected" 15 | self.set_properties(property_dict) 16 | 17 | def update_render_settings(self): 18 | GL.glLineWidth(self._setting_dict["lineWidth"]) 19 | if self._setting_dict["lineType"] == "connected": 20 | self._setting_dict["drawStyle"] = GL.GL_LINE_STRIP 21 | elif self._setting_dict["lineType"] == "loop": 22 | self._setting_dict["drawStyle"] = GL.GL_LINE_LOOP 23 | elif self._setting_dict["lineType"] == "segments": 24 | self._setting_dict["drawStyle"] = GL.GL_LINES 25 | else: 26 | raise Exception("Unknown LineMaterial draw style") -------------------------------------------------------------------------------- /py3d/effects/pixelate.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class PixelateEffect(Material): 5 | """ 6 | Pixelation 7 | """ 8 | def __init__(self, pixel_size=8, resolution=(512, 512)): 9 | vertex_shader_code = """ 10 | in vec2 vertexPosition; 11 | in vec2 vertexUV; 12 | out vec2 UV; 13 | 14 | void main() 15 | { 16 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 17 | UV = vertexUV; 18 | } 19 | """ 20 | fragment_shader_code = """ 21 | in vec2 UV; 22 | uniform sampler2D textureSampler; 23 | uniform float pixelSize; 24 | uniform vec2 resolution; 25 | out vec4 fragColor; 26 | 27 | void main() 28 | { 29 | vec2 factor = resolution / pixelSize; 30 | vec2 newUV = floor(UV * factor) / factor; 31 | vec4 color = texture(textureSampler, newUV); 32 | fragColor = color; 33 | } 34 | """ 35 | super().__init__(vertex_shader_code, fragment_shader_code) 36 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 37 | self.add_uniform("float", "pixelSize", pixel_size) 38 | self.add_uniform("vec2", "resolution", resolution) 39 | self.locate_uniforms() 40 | -------------------------------------------------------------------------------- /py3d/material/surface.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.material.basic import BasicMaterial 4 | 5 | 6 | class SurfaceMaterial(BasicMaterial): 7 | def __init__(self, vertex_shader_code=None, fragment_shader_code=None, property_dict=None, use_vertex_colors=True): 8 | super().__init__(vertex_shader_code, fragment_shader_code, use_vertex_colors) 9 | # Render vertices as surface 10 | self._setting_dict["drawStyle"] = GL.GL_TRIANGLES 11 | # Render both sides? default: front side only 12 | # (vertices ordered counterclockwise) 13 | self._setting_dict["doubleSide"] = False 14 | # Render triangles as wireframe? 15 | self._setting_dict["wireframe"] = False 16 | # Set line thickness for wireframe rendering 17 | self._setting_dict["lineWidth"] = 1 18 | self.set_properties(property_dict) 19 | 20 | def update_render_settings(self): 21 | if self._setting_dict["doubleSide"]: 22 | GL.glDisable(GL.GL_CULL_FACE) 23 | else: 24 | GL.glEnable(GL.GL_CULL_FACE) 25 | if self._setting_dict["wireframe"]: 26 | GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) 27 | else: 28 | GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL) 29 | GL.glLineWidth(self._setting_dict["lineWidth"]) 30 | -------------------------------------------------------------------------------- /py3d/core_ext/mesh.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.core_ext.object3d import Object3D 4 | 5 | 6 | class Mesh(Object3D): 7 | """ 8 | Contains geometric data that specifies vertex-related properties and material data 9 | that specifies the general appearance of the object 10 | """ 11 | def __init__(self, geometry, material): 12 | super().__init__() 13 | self._geometry = geometry 14 | self._material = material 15 | # Should this object be rendered? 16 | self._visible = True 17 | # Set up associations between attributes stored in geometry 18 | # and shader program stored in material 19 | self._vao_ref = GL.glGenVertexArrays(1) 20 | GL.glBindVertexArray(self._vao_ref) 21 | for variable_name, attribute_object in geometry.attribute_dict.items(): 22 | attribute_object.associate_variable(material.program_ref, variable_name) 23 | # Unbind this vertex array object 24 | GL.glBindVertexArray(0) 25 | 26 | @property 27 | def geometry(self): 28 | return self._geometry 29 | 30 | @property 31 | def material(self): 32 | return self._material 33 | 34 | @property 35 | def vao_ref(self): 36 | return self._vao_ref 37 | 38 | @property 39 | def visible(self): 40 | return self._visible 41 | -------------------------------------------------------------------------------- /examples/4-6--axes-and-xy-grid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.renderer import Renderer 7 | from py3d.core_ext.scene import Scene 8 | from py3d.extras.axes import AxesHelper 9 | from py3d.extras.grid import GridHelper 10 | from py3d.extras.movement_rig import MovementRig 11 | 12 | 13 | class Example(Base): 14 | """ 15 | Render axes and a rotated xy-grid. 16 | Move the camera: WASDRF(move), QE(turn), TG(look). 17 | """ 18 | def initialize(self): 19 | print("Initializing program...") 20 | self.renderer = Renderer() 21 | self.scene = Scene() 22 | self.camera = Camera(aspect_ratio=800/600) 23 | self.rig = MovementRig() 24 | self.rig.add(self.camera) 25 | self.rig.set_position([0.5, 1, 5]) 26 | self.scene.add(self.rig) 27 | axes = AxesHelper(axis_length=2) 28 | self.scene.add(axes) 29 | grid = GridHelper( 30 | size=20, 31 | grid_color=[1, 1, 1], 32 | center_color=[1, 1, 0] 33 | ) 34 | grid.rotate_x(-math.pi / 2) 35 | self.scene.add(grid) 36 | 37 | def update(self): 38 | self.rig.update(self.input, self.delta_time) 39 | self.renderer.render(self.scene, self.camera) 40 | 41 | 42 | # Instantiate this class and run the program 43 | Example(screen_size=[800, 600]).run() 44 | -------------------------------------------------------------------------------- /examples/2-02--single-point.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | 7 | 8 | class Example(Base): 9 | """ Render a single point """ 10 | def initialize(self): 11 | print("Initializing program...") 12 | # Initialize program # 13 | # vertex shader code 14 | vs_code = """ 15 | void main() 16 | { 17 | gl_Position = vec4(0.0, 0.0, 0.0, 1.0); 18 | } 19 | """ 20 | # fragment shader code 21 | fs_code = """ 22 | out vec4 fragColor; 23 | void main() 24 | { 25 | fragColor = vec4(1.0, 1.0, 0.0, 1.0); 26 | } 27 | """ 28 | # Send code to GPU and compile; store program reference 29 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 30 | # Set up vertex array object # 31 | vao_ref = GL.glGenVertexArrays(1) 32 | GL.glBindVertexArray(vao_ref) 33 | # render settings (optional) # 34 | # Set point width and height 35 | GL.glPointSize(10) 36 | 37 | def update(self): 38 | # Select program to use when rendering 39 | GL.glUseProgram(self.program_ref) 40 | # Renders geometric objects using selected program 41 | GL.glDrawArrays(GL.GL_POINTS, 0, 1) 42 | 43 | 44 | # Instantiate this class and run the program 45 | Example().run() 46 | 47 | -------------------------------------------------------------------------------- /py3d/geometry/ellipsoid.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from py3d.core.matrix import Matrix 4 | from py3d.geometry.parametric import ParametricGeometry 5 | 6 | 7 | class EllipsoidGeometry(ParametricGeometry): 8 | def __init__(self, width=1, height=1, depth=1, theta_segments=16, phi_segments=32): 9 | def surface_function(u, v): 10 | # [x, y, z] = surface_function(u, v) 11 | # Here, 12 | # x = width / 2 * sin(theta) * cos(phi), 13 | # y = height / 2 * sin(theta) * sin(phi), 14 | # z = depth / 2 * cos(theta), 15 | # where 0 <= theta < pi, 0 <= phi < 2*pi. 16 | # Then, u = phi / (2*pi), v = (1 - theta/pi). 17 | # Then, phi = 2 * pi * u, theta = (1 - v)*pi. 18 | phi = 2 * math.pi * u 19 | theta = (1 - v) * math.pi 20 | return [width / 2 * math.sin(theta) * math.cos(phi), 21 | height / 2 * math.sin(theta) * math.sin(phi), 22 | depth / 2 * math.cos(theta)] 23 | 24 | super().__init__(u_start=0, 25 | u_end=1, 26 | u_resolution=phi_segments, 27 | v_start=0, 28 | v_end=1, 29 | v_resolution=theta_segments, 30 | surface_function=surface_function) 31 | # Rotate the ellipsoid around the x-axis on -90 degrees. 32 | # The vertices and normals will be recalculated. 33 | self.apply_matrix(Matrix.make_rotation_x(-math.pi/2)) 34 | -------------------------------------------------------------------------------- /py3d/material/lighted.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class LightedMaterial(Material): 5 | def __init__(self, number_of_light_sources=1): 6 | self._number_of_light_sources = number_of_light_sources 7 | # Properties vertex_shader_code and fragment_shader_code 8 | # will be defined in inherited classes FlatMaterial, LambertMaterial, 9 | # and PhongMaterial 10 | super().__init__(self.vertex_shader_code, self.fragment_shader_code) 11 | # Add light uniforms to self._uniform_dict 12 | for i in range(self._number_of_light_sources): 13 | self.add_uniform("Light", f"light{i}", None) 14 | 15 | @property 16 | def declaring_light_uniforms_in_shader_code(self): 17 | """ Create a text line with light uniforms to be inserted into a shader code """ 18 | return "\n" + "\n".join(f"\t\t\tuniform Light light{i};" 19 | for i in range(self._number_of_light_sources)) + "\n" 20 | 21 | @property 22 | def adding_lights_in_shader_code(self): 23 | return "\n" + "\n".join(f"\t\t\t\tlight += calculateLight(light{i}, position, calcNormal);" 24 | for i in range(self._number_of_light_sources)) 25 | 26 | @property 27 | def vertex_shader_code(self): 28 | raise NotImplementedError("Implement this property for an inheriting class") 29 | 30 | @property 31 | def fragment_shader_code(self): 32 | raise NotImplementedError("Implement this property for an inheriting class") 33 | -------------------------------------------------------------------------------- /examples/4-1--spinning-cube.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.box import BoxGeometry 8 | from py3d.material.surface import SurfaceMaterial 9 | from py3d.material.point import PointMaterial 10 | 11 | 12 | class Example(Base): 13 | """ Render a basic scene that consists of a spinning cube """ 14 | def initialize(self): 15 | print("Initializing program...") 16 | self.renderer = Renderer() 17 | self.scene = Scene() 18 | self.camera = Camera(aspect_ratio=800/600) 19 | self.camera.set_position([0, 0, 4]) 20 | geometry = BoxGeometry() 21 | # material = PointMaterial(property_dict={"baseColor": [1, 1, 0], "pointSize": 5}) 22 | material = SurfaceMaterial(property_dict={"useVertexColors": True}) 23 | # material = SurfaceMaterial( 24 | # property_dict= { 25 | # "useVertexColors": True, 26 | # "wireframe": True, 27 | # "lineWidth": 8 28 | # } 29 | # ) 30 | self.mesh = Mesh(geometry, material) 31 | self.scene.add(self.mesh) 32 | 33 | def update(self): 34 | self.mesh.rotate_y(0.02514) 35 | self.mesh.rotate_x(0.01337) 36 | self.renderer.render(self.scene, self.camera) 37 | 38 | 39 | # Instantiate this class and run the program 40 | Example(screen_size=[800, 600]).run() -------------------------------------------------------------------------------- /py3d/effects/additive_blend.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class AdditiveBlendEffect(Material): 5 | def __init__(self, blend_texture, original_strength=1, blend_strength=1): 6 | # pass-through shader 7 | vertex_shader_code = """ 8 | in vec2 vertexPosition; 9 | in vec2 vertexUV; 10 | out vec2 UV; 11 | 12 | void main() 13 | { 14 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 15 | UV = vertexUV; 16 | } 17 | """ 18 | # pass-through shader 19 | fragment_shader_code = """ 20 | in vec2 UV; 21 | uniform sampler2D textureSampler; 22 | uniform sampler2D blendTextureSampler; 23 | uniform float originalStrength; 24 | uniform float blendStrength; 25 | out vec4 fragColor; 26 | 27 | void main() 28 | { 29 | vec4 originalColor = texture(textureSampler, UV); 30 | vec4 blendColor = texture(blendTextureSampler, UV); 31 | vec4 color = originalStrength * originalColor + blendStrength * blendColor; 32 | fragColor = color; 33 | } 34 | """ 35 | super().__init__(vertex_shader_code, fragment_shader_code) 36 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 37 | self.add_uniform("sampler2D", "blendTextureSampler", [blend_texture.texture_ref, 2]) 38 | self.add_uniform("float", "originalStrength", original_strength) 39 | self.add_uniform("float", "blendStrength", blend_strength) 40 | self.locate_uniforms() 41 | -------------------------------------------------------------------------------- /examples/4-2--custom-geometry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.geometry import Geometry 8 | from py3d.material.surface import SurfaceMaterial 9 | 10 | 11 | class Example(Base): 12 | """ Render a basic scene that consists of a custom geometry """ 13 | def initialize(self): 14 | print("Initializing program...") 15 | self.renderer = Renderer() 16 | self.scene = Scene() 17 | self.camera = Camera(aspect_ratio=800/600) 18 | self.camera.set_position([0, 0, 0.5]) 19 | geometry = Geometry() 20 | p0 = [-0.1, 0.1, 0.0] 21 | p1 = [0.0, 0.0, 0.0] 22 | p2 = [0.1, 0.1, 0.0] 23 | p3 = [-0.2, -0.2, 0.0] 24 | p4 = [0.2, -0.2, 0.0] 25 | position_data = [p0, p3, p1, p1, p3, p4, p1, p4, p2] 26 | geometry.add_attribute("vec3", "vertexPosition", position_data) 27 | R = [1, 0, 0] 28 | Y = [1, 1, 0] 29 | G = [0, 0.25, 0] 30 | color_data = [R, G, Y, Y, G, G, Y, G, R] 31 | geometry.add_attribute("vec3", "vertexColor", color_data) 32 | material = SurfaceMaterial(property_dict={"useVertexColors": True}) 33 | self.mesh = Mesh(geometry, material) 34 | self.scene.add(self.mesh) 35 | 36 | def update(self): 37 | self.renderer.render(self.scene, self.camera) 38 | 39 | 40 | # Instantiate this class and run the program 41 | Example(screen_size=[800, 600]).run() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenGL examples with PyOpenGL and Pygame 2 | The OpenGL examples are based on the book *"Developing Graphics Frameworks with Python and OpenGL"* by Lee Stemkoski and Michael Pascale published by *CRC Press* in 2021. 3 | 4 | The examples cover all the book chapters with code, 2 through 6, with some code changes and demonstrate **GLSL** programming using **PyOpenGL**. **Pygame** is mainly used for control, windowing, and image loading. 5 | 6 | You find the examples in the `examples` folder. Just read a class description in a script and run it. Since the object-oriented approach is used, auxiliary classes are logically separated in other folders (packages). 7 | 8 | My environment was Python 3.8 with the following packages (without specifying their dependencies here): 9 | ``` 10 | numpy==1.22.4 11 | pygame==2.1.2 12 | PyOpenGL==3.1.6 13 | PyOpenGL-accelerate==3.1.6 14 | ``` 15 | 16 | The code was tested on the same machine with two operating systems, more precisely: 17 | 18 | - OS: Windows 11; Vendor: ATI Technologies Inc.; Renderer: AMD Radeon(TM) Graphics; OpenGL version supported: 4.6.14761 Compatibility Profile Context 21.30.44.03 30.0.13044.3001; GLSL version supported: 4.60 19 | 20 | - OS: Ubuntu 20.04.3 LTS; Vendor: AMD; Renderer: AMD RENOIR (DRM 3.41.0, 5.13.0-48-generic, LLVM 12.0.0); OpenGL version supported: 4.6 (Compatibility Profile) Mesa 21.2.6; GLSL version supported: 4.60 21 | 22 | Update: 23 | 24 | - On Ubuntu, you may encounter the error `OpenGL.error.Error: Attempt to retrieve context when no valid context`. 25 | See a bug report and suggestions to resolve it: https://github.com/pygame/pygame/issues/3110 26 | -------------------------------------------------------------------------------- /py3d/effects/vertical_blur.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class VerticalBlurEffect(Material): 5 | def __init__(self, texture_size=(512, 512), blur_radius=20): 6 | # pass-through shader 7 | vertex_shader_code = """ 8 | in vec2 vertexPosition; 9 | in vec2 vertexUV; 10 | out vec2 UV; 11 | 12 | void main() 13 | { 14 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 15 | UV = vertexUV; 16 | } 17 | """ 18 | # pass-through shader 19 | fragment_shader_code = """ 20 | in vec2 UV; 21 | uniform sampler2D textureSampler; 22 | uniform vec2 textureSize; 23 | uniform int blurRadius; 24 | out vec4 fragColor; 25 | 26 | void main() 27 | { 28 | vec2 pixelToTextureCoords = 1 / textureSize; 29 | vec4 averageColor = vec4(0, 0, 0, 0); 30 | for (int offsetY = -blurRadius; offsetY <= blurRadius; offsetY++) 31 | { 32 | float weight = blurRadius - abs(offsetY) + 1; 33 | vec2 offsetUV = vec2(0, offsetY) * pixelToTextureCoords; 34 | averageColor += texture(textureSampler, UV + offsetUV) * weight; 35 | } 36 | averageColor /= averageColor.a; 37 | fragColor = averageColor; 38 | } 39 | """ 40 | super().__init__(vertex_shader_code, fragment_shader_code) 41 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 42 | self.add_uniform("vec2", "textureSize", texture_size) 43 | self.add_uniform("int", "blurRadius", blur_radius) 44 | self.locate_uniforms() 45 | -------------------------------------------------------------------------------- /py3d/effects/horizontal_blur.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class HorizontalBlurEffect(Material): 5 | def __init__(self, texture_size=(512, 512), blur_radius=20): 6 | # pass-through shader 7 | vertex_shader_code = """ 8 | in vec2 vertexPosition; 9 | in vec2 vertexUV; 10 | out vec2 UV; 11 | 12 | void main() 13 | { 14 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 15 | UV = vertexUV; 16 | } 17 | """ 18 | # pass-through shader 19 | fragment_shader_code = """ 20 | in vec2 UV; 21 | uniform sampler2D textureSampler; 22 | uniform vec2 textureSize; 23 | uniform int blurRadius; 24 | out vec4 fragColor; 25 | 26 | void main() 27 | { 28 | vec2 pixelToTextureCoords = 1 / textureSize; 29 | vec4 averageColor = vec4(0, 0, 0, 0); 30 | for (int offsetX = -blurRadius; offsetX <= blurRadius; offsetX++) 31 | { 32 | float weight = blurRadius - abs(offsetX) + 1; 33 | vec2 offsetUV = vec2(offsetX, 0) * pixelToTextureCoords; 34 | averageColor += texture(textureSampler, UV + offsetUV) * weight; 35 | } 36 | averageColor /= averageColor.a; 37 | fragColor = averageColor; 38 | } 39 | """ 40 | super().__init__(vertex_shader_code, fragment_shader_code) 41 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 42 | self.add_uniform("vec2", "textureSize", texture_size) 43 | self.add_uniform("int", "blurRadius", blur_radius) 44 | self.locate_uniforms() 45 | -------------------------------------------------------------------------------- /py3d/geometry/rectangle.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.geometry import Geometry 2 | 3 | 4 | class RectangleGeometry(Geometry): 5 | def __init__(self, width=1, height=1, position=(0, 0), alignment=(0.5, 0.5)): 6 | super().__init__() 7 | # vertices 8 | # p2 - p3 9 | # | / | 10 | # p0 - p1 11 | # p0 = [-width/2, -height/2, 0] 12 | # p1 = [ width/2, -height/2, 0] 13 | # p2 = [-width/2, height/2, 0] 14 | # p3 = [ width/2, height/2, 0] 15 | x, y = position 16 | a, b = alignment 17 | p0 = [x + (-a) * width, y + (-b) * height, 0] 18 | p1 = [x + (1 - a) * width, y + (-b) * height, 0] 19 | p2 = [x + (-a) * width, y + (1 - b) * height, 0] 20 | p3 = [x + (1 - a) * width, y + (1 - b) * height, 0] 21 | # colors 22 | c0, c1, c2, c3 = [1, 1, 1], [1, 0, 0], [0, 1, 0], [0, 0, 1] 23 | # texture coordinates 24 | t0, t1, t2, t3 = [0, 0], [1, 0], [0, 1], [1, 1] 25 | # triangles p0-p1-p3 and p0-p3-p2 26 | # p2 - p3 27 | # | / | 28 | # p0 - p1 29 | position_data = [p0, p1, p3, p0, p3, p2] 30 | color_data = [c0, c1, c3, c0, c3, c2] 31 | # color_data = [c0, c0, c0, c1, c1, c1] 32 | uv_data = [t0, t1, t3, t0, t3, t2] 33 | self.add_attribute("vec3", "vertexPosition", position_data) 34 | self.add_attribute("vec3", "vertexColor", color_data) 35 | self.add_attribute("vec2", "vertexUV", uv_data) 36 | normal_data = [[0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]] 37 | self.add_attribute("vec3", "vertexNormal", normal_data) 38 | self.add_attribute("vec3", "faceNormal", normal_data) 39 | -------------------------------------------------------------------------------- /py3d/material/basic.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | from py3d.core.uniform import Uniform 3 | 4 | 5 | class BasicMaterial(Material): 6 | def __init__(self, vertex_shader_code=None, fragment_shader_code=None, use_vertex_colors=True): 7 | if vertex_shader_code is None: 8 | vertex_shader_code = """ 9 | uniform mat4 projectionMatrix; 10 | uniform mat4 viewMatrix; 11 | uniform mat4 modelMatrix; 12 | in vec3 vertexPosition; 13 | in vec3 vertexColor; 14 | out vec3 color; 15 | 16 | void main() 17 | { 18 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); 19 | color = vertexColor; 20 | } 21 | """ 22 | if fragment_shader_code is None: 23 | fragment_shader_code = """ 24 | uniform vec3 baseColor; 25 | uniform bool useVertexColors; 26 | in vec3 color; 27 | out vec4 fragColor; 28 | 29 | void main() 30 | { 31 | fragColor = vec4(baseColor, 1.0); 32 | if (useVertexColors) 33 | { 34 | fragColor = vec4(color, 1.0); 35 | } 36 | } 37 | """ 38 | super().__init__(vertex_shader_code, fragment_shader_code) 39 | self.add_uniform("vec3", "baseColor", [1.0, 1.0, 1.0]) 40 | if use_vertex_colors: 41 | self.add_uniform("bool", "useVertexColors", False) 42 | self.locate_uniforms() 43 | -------------------------------------------------------------------------------- /examples/5-07--text-texture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.rectangle import RectangleGeometry 8 | from py3d.extras.text_texture import TextTexture 9 | from py3d.material.texture import TextureMaterial 10 | 11 | 12 | class Example(Base): 13 | """ Render a rotating rectangle with a text texture """ 14 | def initialize(self): 15 | print("Initializing program...") 16 | self.renderer = Renderer() 17 | self.scene = Scene() 18 | self.camera = Camera(aspect_ratio=800/600) 19 | self.camera.set_position([0, 0, 1.5]) 20 | geometry = RectangleGeometry() 21 | message = TextTexture(text="Python Graphics", 22 | system_font_name="Impact", 23 | font_size=32, 24 | font_color=[0, 0, 200], 25 | image_width=256, 26 | image_height=256, 27 | align_horizontal=0.5, 28 | align_vertical=0.5, 29 | image_border_width=4, 30 | image_border_color=[255, 0, 0]) 31 | material = TextureMaterial(message) 32 | self.mesh = Mesh(geometry, material) 33 | self.scene.add(self.mesh) 34 | 35 | def update(self): 36 | self.mesh.rotate_y(0.0114) 37 | self.mesh.rotate_x(0.0237) 38 | self.renderer.render(self.scene, self.camera) 39 | 40 | 41 | # Instantiate this class and run the program 42 | Example(screen_size=[800, 600]).run() 43 | -------------------------------------------------------------------------------- /examples/4-7--box-on-grid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.geometry.box import BoxGeometry 10 | from py3d.extras.axes import AxesHelper 11 | from py3d.extras.grid import GridHelper 12 | from py3d.extras.movement_rig import MovementRig 13 | from py3d.material.surface import SurfaceMaterial 14 | 15 | 16 | class Example(Base): 17 | """ 18 | Render axes, a rotated xy-grid, and a box. 19 | Move the box: WASDRF(move), QE(turn), TG(look). 20 | """ 21 | def initialize(self): 22 | print("Initializing program...") 23 | self.renderer = Renderer() 24 | self.scene = Scene() 25 | self.camera = Camera(aspect_ratio=800/600) 26 | self.camera.set_position([0, 1, 5]) 27 | geometry = BoxGeometry() 28 | material = SurfaceMaterial(property_dict={"useVertexColors": True}) 29 | self.mesh = Mesh(geometry, material) 30 | self.rig = MovementRig() 31 | self.rig.add(self.mesh) 32 | self.rig.set_position([0, 0.5, 0]) 33 | self.scene.add(self.rig) 34 | axes = AxesHelper(axis_length=2) 35 | self.scene.add(axes) 36 | grid = GridHelper( 37 | size=20, 38 | grid_color=[1, 1, 1], 39 | center_color=[1, 1, 0] 40 | ) 41 | grid.rotate_x(-math.pi / 2) 42 | self.scene.add(grid) 43 | 44 | def update(self): 45 | self.rig.update(self.input, self.delta_time) 46 | self.renderer.render(self.scene, self.camera) 47 | 48 | 49 | # Instantiate this class and run the program 50 | Example(screen_size=[800, 600]).run() 51 | -------------------------------------------------------------------------------- /examples/template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.texture import Texture 8 | from py3d.geometry.box import BoxGeometry 9 | # from py3d.geometry.geometry import Geometry 10 | from py3d.material.surface import SurfaceMaterial 11 | from py3d.material.texture import TextureMaterial 12 | from py3d.geometry.sphere import SphereGeometry 13 | from py3d.light.ambient import AmbientLight 14 | from py3d.light.directional import DirectionalLight 15 | from py3d.light.point import PointLight 16 | from py3d.material.flat import FlatMaterial 17 | from py3d.material.lambert import LambertMaterial 18 | from py3d.material.phong import PhongMaterial 19 | 20 | 21 | class Example(Base): 22 | """ Example template """ 23 | def initialize(self): 24 | print("Initializing program...") 25 | self.renderer = Renderer() 26 | self.scene = Scene() 27 | self.camera = Camera(aspect_ratio=800/600) 28 | self.camera.set_position([0, 0, 4]) 29 | geometry = BoxGeometry() 30 | # material = SurfaceMaterial(property_dict={"useVertexColors": True}) 31 | material = SurfaceMaterial( 32 | property_dict={ 33 | "useVertexColors": True, 34 | "wireframe": True, 35 | "lineWidth": 8 36 | } 37 | ) 38 | self.mesh = Mesh(geometry, material) 39 | self.scene.add(self.mesh) 40 | 41 | def update(self): 42 | self.mesh.rotate_y(0.0514) 43 | self.mesh.rotate_x(0.0337) 44 | self.renderer.render(self.scene, self.camera) 45 | 46 | 47 | # Instantiate this class and run the program 48 | Example(screen_size=[800, 600]).run() 49 | -------------------------------------------------------------------------------- /examples/4-4--sphere.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.sphere import SphereGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ Render a spinning sphere with gradient colors """ 13 | def initialize(self): 14 | print("Initializing program...") 15 | self.renderer = Renderer() 16 | self.scene = Scene() 17 | self.camera = Camera(aspect_ratio=800/600) 18 | self.camera.set_position([0, 0, 7]) 19 | geometry = SphereGeometry(radius=3) 20 | vs_code = """ 21 | uniform mat4 modelMatrix; 22 | uniform mat4 viewMatrix; 23 | uniform mat4 projectionMatrix; 24 | in vec3 vertexPosition; 25 | out vec3 position; 26 | void main() 27 | { 28 | vec4 pos = vec4(vertexPosition, 1.0); 29 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos; 30 | position = vertexPosition; 31 | } 32 | """ 33 | fs_code = """ 34 | in vec3 position; 35 | out vec4 fragColor; 36 | void main() 37 | { 38 | vec3 color = mod(position, 1.0); 39 | fragColor = vec4(color, 1.0); 40 | } 41 | """ 42 | material = Material(vs_code, fs_code) 43 | material.locate_uniforms() 44 | self.mesh = Mesh(geometry, material) 45 | self.scene.add(self.mesh) 46 | 47 | def update(self): 48 | self.mesh.rotate_y(0.00514) 49 | self.mesh.rotate_x(0.00337) 50 | self.renderer.render(self.scene, self.camera) 51 | 52 | 53 | # Instantiate this class and run the program 54 | Example(screen_size=[800, 600]).run() -------------------------------------------------------------------------------- /py3d/extras/grid.py: -------------------------------------------------------------------------------- 1 | from py3d.core_ext.mesh import Mesh 2 | from py3d.geometry.geometry import Geometry 3 | from py3d.material.line import LineMaterial 4 | 5 | 6 | class GridHelper(Mesh): 7 | def __init__(self, size=10, divisions=10, grid_color=(0, 0, 0), center_color=(0.5, 0.5, 0.5), line_width=1): 8 | geometry = Geometry() 9 | position_data = [] 10 | color_data = [] 11 | # Create range of values 12 | values = [] 13 | delta_size = size / divisions 14 | for n in range(divisions + 1): 15 | values.append(-size / 2 + n * delta_size) 16 | # Add vertical lines 17 | for x in values: 18 | position_data.append([x, -size / 2, 0]) 19 | position_data.append([x, size / 2, 0]) 20 | if x == 0: 21 | color_data.append(center_color) 22 | color_data.append(center_color) 23 | else: 24 | color_data.append(grid_color) 25 | color_data.append(grid_color) 26 | # Add horizontal lines 27 | for y in values: 28 | position_data.append([-size / 2, y, 0]) 29 | position_data.append([size / 2, y, 0]) 30 | if y == 0: 31 | color_data.append(center_color) 32 | color_data.append(center_color) 33 | else: 34 | color_data.append(grid_color) 35 | color_data.append(grid_color) 36 | geometry.add_attribute("vec3", "vertexPosition", position_data) 37 | geometry.add_attribute("vec3", "vertexColor", color_data) 38 | material = LineMaterial( 39 | property_dict = { 40 | "useVertexColors": 1, 41 | "lineWidth": line_width, 42 | "lineType": "segments" 43 | } 44 | ) 45 | # Initialize the mesh 46 | super().__init__(geometry, material) 47 | -------------------------------------------------------------------------------- /examples/5-02--textured-skysphere-and-grass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | 15 | 16 | class Example(Base): 17 | """ 18 | Render a textured skysphere and a textured grass floor. 19 | Move the camera: WASDRF(move), QE(turn), TG(look). 20 | """ 21 | def initialize(self): 22 | print("Initializing program...") 23 | self.renderer = Renderer() 24 | self.scene = Scene() 25 | self.camera = Camera(aspect_ratio=800/600) 26 | self.rig = MovementRig() 27 | self.rig.add(self.camera) 28 | self.scene.add(self.rig) 29 | self.rig.set_position([0, 1, 4]) 30 | sky_geometry = SphereGeometry(radius=50) 31 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 32 | sky = Mesh(sky_geometry, sky_material) 33 | self.scene.add(sky) 34 | grass_geometry = RectangleGeometry(width=100, height=100) 35 | grass_material = TextureMaterial( 36 | texture=Texture(file_name="../py3d/images/grass.jpg"), 37 | property_dict={"repeatUV": [50, 50]} 38 | ) 39 | grass = Mesh(grass_geometry, grass_material) 40 | grass.rotate_x(-math.pi/2) 41 | self.scene.add(grass) 42 | 43 | def update(self): 44 | self.rig.update(self.input, self.delta_time) 45 | self.renderer.render(self.scene, self.camera) 46 | 47 | 48 | # Instantiate this class and run the program 49 | Example(screen_size=[800, 600]).run() 50 | -------------------------------------------------------------------------------- /examples/2-03--hexagon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | from py3d.core.attribute import Attribute 7 | 8 | 9 | class Example(Base): 10 | """ Render six points in a hexagon arrangement """ 11 | def initialize(self): 12 | print("Initializing program...") 13 | # Initialize program # 14 | vs_code = """ 15 | in vec3 position; 16 | void main() 17 | { 18 | gl_Position = vec4(position.x, position.y, position.z, 1.0); 19 | } 20 | """ 21 | fs_code = """ 22 | out vec4 fragColor; 23 | void main() 24 | { 25 | fragColor = vec4(1.0, 1.0, 0.0, 1.0); 26 | } 27 | """ 28 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 29 | # Render settings (optional) # 30 | GL.glLineWidth(4) 31 | # Set up vertex array object # 32 | vao_ref = GL.glGenVertexArrays(1) 33 | GL.glBindVertexArray(vao_ref) 34 | # Set up vertex attribute # 35 | position_data = [[ 0.8, 0.0, 0.0], 36 | [ 0.4, 0.6, 0.0], 37 | [-0.4, 0.6, 0.0], 38 | [-0.8, 0.0, 0.0], 39 | [-0.4, -0.6, 0.0], 40 | [ 0.4, -0.6, 0.0]] 41 | self.vertex_count = len(position_data) 42 | position_attribute = Attribute('vec3', position_data) 43 | position_attribute.associate_variable(self.program_ref, 'position') 44 | 45 | def update(self): 46 | GL.glUseProgram(self.program_ref) 47 | # GL.glDrawArrays(GL.GL_LINE_LOOP, 0 , self.vertex_count) 48 | # GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 49 | GL.glDrawArrays(GL.GL_TRIANGLE_FAN, 0, self.vertex_count) 50 | 51 | 52 | # Instantiate this class and run the program 53 | Example().run() -------------------------------------------------------------------------------- /py3d/effects/vignette.py: -------------------------------------------------------------------------------- 1 | from py3d.material.material import Material 2 | 3 | 4 | class VignetteEffect(Material): 5 | """ 6 | Vignette Effect 7 | """ 8 | def __init__(self, 9 | dimming_start=0.4, 10 | dimming_end=1.0, 11 | dimming_color=(0, 0, 0)): 12 | vertex_shader_code = """ 13 | in vec2 vertexPosition; 14 | in vec2 vertexUV; 15 | out vec2 UV; 16 | 17 | void main() 18 | { 19 | gl_Position = vec4(vertexPosition, 0.0, 1.0); 20 | UV = vertexUV; 21 | } 22 | """ 23 | fragment_shader_code = """ 24 | in vec2 UV; 25 | uniform sampler2D textureSampler; 26 | uniform float dimStart; 27 | uniform float dimEnd; 28 | uniform vec3 dimColor; 29 | out vec4 fragColor; 30 | 31 | void main() 32 | { 33 | vec4 color = texture(textureSampler, UV); 34 | // Calculate position in clip space from UV coordinates 35 | vec2 position = 2 * UV - vec2(1, 1); 36 | // Calculate distance from center, which affects brightness 37 | float d = length(position); 38 | // Calculate brightness factor 39 | // if d = dimStart, then b = 1; if d = dimEnd, then b = 0 40 | float b = (d - dimEnd) / (dimStart - dimEnd); 41 | // Prevent oversaturation 42 | b = clamp(b, 0, 1); 43 | // Mix the texture color and dim color 44 | fragColor = vec4(b * color.rgb + (1 - b) * dimColor, 1); 45 | } 46 | """ 47 | super().__init__(vertex_shader_code, fragment_shader_code) 48 | self.add_uniform("sampler2D", "textureSampler", [None, 1]) 49 | self.add_uniform("float", "dimStart", dimming_start) 50 | self.add_uniform("float", "dimEnd", dimming_end) 51 | self.add_uniform("vec3", "dimColor", dimming_color) 52 | self.locate_uniforms() 53 | -------------------------------------------------------------------------------- /py3d/geometry/box.py: -------------------------------------------------------------------------------- 1 | from py3d.geometry.geometry import Geometry 2 | 3 | 4 | class BoxGeometry(Geometry): 5 | def __init__(self, width=1, height=1, depth=1): 6 | super().__init__() 7 | # vertices 8 | p0 = [-width / 2, -height / 2, -depth / 2] 9 | p1 = [width / 2, -height / 2, -depth / 2] 10 | p2 = [-width / 2, height / 2, -depth / 2] 11 | p3 = [width / 2, height / 2, -depth / 2] 12 | p4 = [-width / 2, -height / 2, depth / 2] 13 | p5 = [width / 2, -height / 2, depth / 2] 14 | p6 = [-width / 2, height / 2, depth / 2] 15 | p7 = [width / 2, height / 2, depth / 2] 16 | # colors for faces in order: 17 | # x+, x-, y+, y-, z+, z- 18 | c1, c2 = [1, 0.5, 0.5], [0.5, 0, 0] 19 | c3, c4 = [0.5, 1, 0.5], [0, 0.5, 0] 20 | c5, c6 = [0.5, 0.5, 1], [0, 0, 0.5] 21 | # texture coordinates 22 | t0, t1, t2, t3 = [0, 0], [1, 0], [0, 1], [1, 1] 23 | # Each side consists of two triangles 24 | position_data = [p5, p1, p3, p5, p3, p7, 25 | p0, p4, p6, p0, p6, p2, 26 | p6, p7, p3, p6, p3, p2, 27 | p0, p1, p5, p0, p5, p4, 28 | p4, p5, p7, p4, p7, p6, 29 | p1, p0, p2, p1, p2, p3] 30 | color_data = [c1] * 6 + [c2] * 6 + [c3] * 6 \ 31 | + [c4] * 6 + [c5] * 6 + [c6] * 6 32 | uv_data = [t0, t1, t3, t0, t3, t2] * 6 33 | self.add_attribute("vec3", "vertexPosition", position_data) 34 | self.add_attribute("vec3", "vertexColor", color_data) 35 | self.add_attribute("vec2", "vertexUV", uv_data) 36 | # normal vectors for x+, x-, y+, y-, z+, z- 37 | n1, n2 = [1, 0, 0], [-1, 0, 0] 38 | n3, n4 = [0, 1, 0], [0, -1, 0] 39 | n5, n6 = [0, 0, 1], [0, 0, -1] 40 | normal_data = [n1]*6 + [n2]*6 + [n3]*6 + [n4]*6 + [n5]*6 + [n6]*6 41 | self.add_attribute("vec3", "vertexNormal", normal_data) 42 | self.add_attribute("vec3", "faceNormal", normal_data) 43 | -------------------------------------------------------------------------------- /py3d/light/shadow.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.render_target import RenderTarget 5 | from py3d.material.depth import DepthMaterial 6 | 7 | 8 | class Shadow: 9 | def __init__(self, 10 | light_source, 11 | strength=0.5, 12 | resolution=(512, 512), 13 | camera_bounds=(-5, 5, -5, 5, 0, 20), 14 | bias=0.01): 15 | 16 | # Must be directional light 17 | self._light_source = light_source 18 | # Camera used to render scene from perspective of light 19 | self._camera = Camera() 20 | left, right, bottom, top, near, far = camera_bounds 21 | self._camera.set_orthographic(left, right, bottom, top, near, far) 22 | self._light_source.add(self._camera) 23 | # Target used during the shadow pass, contains depth texture 24 | self._render_target = RenderTarget( 25 | resolution, 26 | property_dict={"wrap": GL.GL_CLAMP_TO_BORDER} 27 | ) 28 | # Render only depth data to target texture 29 | self._material = DepthMaterial() 30 | # Controls darkness of shadow 31 | self._strength = strength 32 | # Used to avoid visual artifacts due to 33 | # rounding / sampling precision issues 34 | self._bias = bias 35 | 36 | @property 37 | def bias(self): 38 | return self._bias 39 | 40 | @property 41 | def camera(self): 42 | return self._camera 43 | 44 | @property 45 | def material(self): 46 | return self._material 47 | 48 | @property 49 | def light_source(self): 50 | return self._light_source 51 | 52 | @property 53 | def render_target(self): 54 | return self._render_target 55 | 56 | @property 57 | def strength(self): 58 | return self._strength 59 | 60 | def update_internal(self): 61 | self._camera.update_view_matrix() 62 | self._material.uniform_dict["viewMatrix"].data = self._camera.view_matrix 63 | self._material.uniform_dict["projectionMatrix"].data = self._camera.projection_matrix 64 | -------------------------------------------------------------------------------- /py3d/core/input.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | class Input: 5 | def __init__(self): 6 | # Has the user quit the application? 7 | self._quit = False 8 | # lists to store key states 9 | # down, up: discrete event; lasts for one iteration 10 | # pressed: continuous event, between down and up events 11 | self._key_down_list = [] 12 | self._key_pressed_list = [] 13 | self._key_up_list = [] 14 | 15 | @property 16 | def key_down_list(self): 17 | return self._key_down_list 18 | 19 | @property 20 | def key_pressed_list(self): 21 | return self._key_pressed_list 22 | 23 | @property 24 | def key_up_list(self): 25 | return self._key_up_list 26 | 27 | @property 28 | def quit(self): 29 | return self._quit 30 | 31 | # functions to check key states 32 | def is_key_down(self, key_code): 33 | return key_code in self._key_down_list 34 | 35 | def is_key_pressed(self, key_code): 36 | return key_code in self._key_pressed_list 37 | 38 | def is_key_up(self, key_code): 39 | return key_code in self._key_up_list 40 | 41 | def update(self): 42 | # Reset discrete key states 43 | self._key_down_list = [] 44 | self._key_up_list = [] 45 | # Iterate over all user input events (such as keyboard or mouse) 46 | # that occurred since the last time events were checked 47 | for event in pygame.event.get(): 48 | # Quit event occurs by clicking button to close window 49 | if event.type == pygame.QUIT: 50 | self._quit = True 51 | # Check for key-down and key-up events; 52 | # get name of key from event and append to or remove from corresponding lists 53 | if event.type == pygame.KEYDOWN: 54 | key_name = pygame.key.name(event.key) 55 | self._key_down_list.append(key_name) 56 | self._key_pressed_list.append(key_name) 57 | if event.type == pygame.KEYUP: 58 | key_name = pygame.key.name(event.key) 59 | self._key_pressed_list.remove(key_name) 60 | self._key_up_list.append(key_name) -------------------------------------------------------------------------------- /py3d/geometry/cylindrical.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from py3d.core.matrix import Matrix 4 | from py3d.geometry.parametric import ParametricGeometry 5 | from py3d.geometry.polygon import PolygonGeometry 6 | 7 | 8 | class CylindricalGeometry(ParametricGeometry): 9 | def __init__(self, radius_top=1, radius_bottom=1, height=1, 10 | radial_segments=32, height_segments=16, 11 | closed_top=True, closed_bottom=True): 12 | def surface_function(u, v): 13 | # x = radius * cos(phi), 14 | # y = radius * sin(phi), 15 | # z = z, 16 | # where 0 <= phi < 2*pi. 17 | # Then, u = phi / (2*pi), v = z. 18 | # Then, phi = 2 * pi * u, z = v. 19 | phi = 2 * math.pi * u 20 | return [(v*radius_top + (1 - v)*radius_bottom) * math.cos(phi), 21 | (v*radius_top + (1 - v)*radius_bottom) * math.sin(phi), 22 | height*(v - 0.5)] 23 | 24 | super().__init__(u_start=0, 25 | u_end=1, 26 | u_resolution=radial_segments, 27 | v_start=0, 28 | v_end=1, 29 | v_resolution=height_segments, 30 | surface_function=surface_function) 31 | 32 | if closed_top: 33 | top_geometry = PolygonGeometry( 34 | sides=radial_segments, 35 | radius=radius_top, 36 | normals_up=True 37 | ) 38 | transform = Matrix.make_translation(0, 0, height/2) 39 | top_geometry.apply_matrix(transform) 40 | self.merge(top_geometry) 41 | 42 | if closed_bottom: 43 | bottom_geometry = PolygonGeometry( 44 | sides=radial_segments, 45 | radius=radius_bottom, 46 | normals_up=False 47 | ) 48 | transform = Matrix.make_translation(0, 0, -height/2) 49 | bottom_geometry.apply_matrix(transform) 50 | self.merge(bottom_geometry) 51 | 52 | # Rotate around the x-axis on -90 degrees. 53 | # The vertices and normals will be recalculated. 54 | self.apply_matrix(Matrix.make_rotation_x(-math.pi/2)) 55 | -------------------------------------------------------------------------------- /examples/5-09--billboarding-by-spritesheet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.material.sprite import SpriteMaterial 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.grid import GridHelper 15 | 16 | 17 | class Example(Base): 18 | """ 19 | Demonstrate billboarding by the 4-by-3 spritesheet of Sonic. 20 | A billboard always faces a camera. 21 | Move the camera: WASDRF(move), QE(turn), TG(look). 22 | """ 23 | def initialize(self): 24 | print("Initializing program...") 25 | self.renderer = Renderer() 26 | self.scene = Scene() 27 | self.camera = Camera(aspect_ratio=800/600) 28 | self.rig = MovementRig() 29 | self.rig.add(self.camera) 30 | self.rig.set_position([0, 0.5, 3]) 31 | self.scene.add(self.rig) 32 | geometry = RectangleGeometry() 33 | tile_set = Texture("../py3d/images/sonic-spritesheet.jpg") 34 | sprite_material = SpriteMaterial( 35 | tile_set, 36 | { 37 | "billboard": True, 38 | "tileCount": [4, 3], 39 | "tileNumber": 0 40 | } 41 | ) 42 | self.tiles_per_second = 8 43 | self.sprite = Mesh(geometry, sprite_material) 44 | self.scene.add(self.sprite) 45 | grid = GridHelper( 46 | size=20, 47 | grid_color=[1, 1, 1], 48 | center_color=[1, 1, 0] 49 | ) 50 | grid.rotate_x(-math.pi / 2) 51 | self.scene.add(grid) 52 | 53 | def update(self): 54 | tile_number = math.floor(self.time * self.tiles_per_second) 55 | self.sprite.material.uniform_dict["tileNumber"].data = tile_number 56 | self.rig.update(self.input, self.delta_time) 57 | self.renderer.render(self.scene, self.camera) 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /py3d/core_ext/render_target.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | import pygame 3 | 4 | from py3d.core_ext.texture import Texture 5 | 6 | 7 | class RenderTarget: 8 | """ 9 | Create a framebuffer as the target when rendering 10 | """ 11 | def __init__(self, resolution=(512, 512), texture=None, property_dict=None): 12 | # Values should equal texture dimensions 13 | self._width, self._height = resolution 14 | if texture is not None: 15 | self._texture = texture 16 | else: 17 | self._texture = Texture( 18 | file_name=None, 19 | property_dict={ 20 | "magFilter": GL.GL_LINEAR, 21 | "minFilter": GL.GL_LINEAR, 22 | "wrap": GL.GL_CLAMP_TO_EDGE 23 | } 24 | ) 25 | self._texture.set_properties(property_dict) 26 | self._texture.surface = pygame.Surface(resolution) 27 | self._texture.upload_data() 28 | # Create a framebuffer 29 | self._framebuffer_ref = GL.glGenFramebuffers(1) 30 | GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, self._framebuffer_ref) 31 | # Configure color buffer to use this texture 32 | GL.glFramebufferTexture(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, 33 | self._texture.texture_ref, 0) 34 | # Generate a buffer to store depth information 35 | depth_buffer_ref = GL.glGenRenderbuffers(1) 36 | GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, depth_buffer_ref) 37 | GL.glRenderbufferStorage(GL.GL_RENDERBUFFER, GL.GL_DEPTH_COMPONENT, self._width, self._height) 38 | GL.glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, GL.GL_DEPTH_ATTACHMENT, GL.GL_RENDERBUFFER, depth_buffer_ref) 39 | # Check framebuffer status 40 | if GL.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) != GL.GL_FRAMEBUFFER_COMPLETE: 41 | raise Exception("Framebuffer status error") 42 | 43 | @property 44 | def framebuffer_ref(self): 45 | return self._framebuffer_ref 46 | 47 | @property 48 | def height(self): 49 | return self._height 50 | 51 | @property 52 | def width(self): 53 | return self._width 54 | 55 | @property 56 | def texture(self): 57 | return self._texture 58 | -------------------------------------------------------------------------------- /examples/4-3--sine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import numpy as np 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.geometry.geometry import Geometry 10 | from py3d.material.point import PointMaterial 11 | from py3d.material.line import LineMaterial 12 | 13 | 14 | class Example(Base): 15 | """ Render the sine function """ 16 | def initialize(self): 17 | print("Initializing program...") 18 | self.renderer = Renderer() 19 | self.scene = Scene() 20 | self.camera = Camera(aspect_ratio=800/600) 21 | self.camera.set_position([0, 0, 5]) 22 | geometry = Geometry() 23 | vs_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | void main() 29 | { 30 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); 31 | } 32 | """ 33 | fs_code = """ 34 | uniform vec3 baseColor; 35 | uniform bool useVertexColors; 36 | out vec4 fragColor; 37 | void main() 38 | { 39 | fragColor = vec4(baseColor, 1.0); 40 | } 41 | """ 42 | position_data = [] 43 | x_values = np.arange(-3.2, 3.2, 0.2) 44 | y_values = np.sin(x_values) 45 | for x, y in zip(x_values, y_values): 46 | position_data.append([x, y, 0]) 47 | geometry.add_attribute("vec3", "vertexPosition", position_data) 48 | use_vertex_colors = False 49 | point_material = PointMaterial(vs_code, fs_code, {"baseColor": [1, 1, 0], "pointSize": 10}, use_vertex_colors) 50 | point_mesh = Mesh(geometry, point_material) 51 | line_material = LineMaterial(vs_code, fs_code, {"baseColor": [1, 0, 1], "lineWidth": 4}, use_vertex_colors) 52 | line_mesh = Mesh(geometry, line_material) 53 | self.scene.add(point_mesh) 54 | self.scene.add(line_mesh) 55 | 56 | def update(self): 57 | self.renderer.render(self.scene, self.camera) 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() -------------------------------------------------------------------------------- /py3d/geometry/polygon.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from py3d.geometry.geometry import Geometry 4 | 5 | 6 | class PolygonGeometry(Geometry): 7 | """ Symmetrical polygon inscribed in a circle """ 8 | def __init__(self, sides=3, radius=1, normals_up=True): 9 | sides = int(sides) 10 | if sides < 3: 11 | raise ValueError(f"the 'sides' parameter must be at least three") 12 | super().__init__() 13 | delta_phi = 2 * math.pi / sides 14 | position_data = [] 15 | color_data = [] 16 | uv_data = [] 17 | uv_center = [0.5, 0.5] 18 | normal_data = [] 19 | normal_vector = [0, 0, 1] if normals_up else [0, 0, -1] 20 | for n in range(sides): 21 | position_data.append([0, 0, 0]) 22 | position_data.append([radius * math.cos(n * delta_phi), radius * math.sin(n * delta_phi), 0]) 23 | position_data.append([radius * math.cos((n + 1) * delta_phi), radius * math.sin((n + 1) * delta_phi), 0]) 24 | color_data.append([1, 1, 1]) 25 | color_data.append([1, 0, 0]) 26 | color_data.append([0, 0, 1]) 27 | uv_data.append(uv_center) 28 | uv_data.append([math.cos(n * delta_phi) * 0.5 + 0.5, math.sin(n * delta_phi) * 0.5 + 0.5]) 29 | uv_data.append([math.cos((n + 1) * delta_phi) * 0.5 + 0.5, math.sin((n + 1) * delta_phi) * 0.5 + 0.5]) 30 | # Repeat three times for three vertices of triangle 31 | for i in range(3): 32 | normal_data.append(normal_vector.copy()) 33 | self.add_attribute("vec3", "vertexPosition", position_data) 34 | self.add_attribute("vec3", "vertexColor", color_data) 35 | self.add_attribute("vec2", "vertexUV", uv_data) 36 | self.add_attribute("vec3", "vertexNormal", normal_data) 37 | self.add_attribute("vec3", "faceNormal", normal_data) 38 | 39 | @staticmethod 40 | def create_triangle_geometry(radius=1): 41 | return PolygonGeometry(sides=3, radius=radius) 42 | 43 | @staticmethod 44 | def create_square_geometry(radius=1): 45 | return PolygonGeometry(sides=4, radius=radius) 46 | 47 | @staticmethod 48 | def create_pentagon_geometry(radius=1): 49 | return PolygonGeometry(sides=5, radius=radius) 50 | 51 | @staticmethod 52 | def create_hexagon_geometry(radius=1): 53 | return PolygonGeometry(sides=6, radius=radius) -------------------------------------------------------------------------------- /examples/5-12-3--postprocessing-pixelation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.pixelate import PixelateEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate pixelation 21 | """ 22 | def initialize(self): 23 | print("Initializing program...") 24 | self.renderer = Renderer() 25 | self.scene = Scene() 26 | self.camera = Camera(aspect_ratio=800/600) 27 | self.rig = MovementRig() 28 | self.rig.add(self.camera) 29 | self.scene.add(self.rig) 30 | self.rig.set_position([0, 1, 4]) 31 | sky_geometry = SphereGeometry(radius=50) 32 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 33 | sky = Mesh(sky_geometry, sky_material) 34 | self.scene.add(sky) 35 | 36 | grass_geometry = RectangleGeometry(width=100, height=100) 37 | grass_material = TextureMaterial( 38 | texture=Texture(file_name="../py3d/images/grass.jpg"), 39 | property_dict={"repeatUV": [50, 50]} 40 | ) 41 | grass = Mesh(grass_geometry, grass_material) 42 | grass.rotate_x(-math.pi/2) 43 | self.scene.add(grass) 44 | 45 | sphere_geometry = SphereGeometry() 46 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 47 | self.sphere = Mesh(sphere_geometry, sphere_material) 48 | self.sphere.set_position([0, 1, 0]) 49 | self.scene.add(self.sphere) 50 | 51 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 52 | self.postprocessor.add_effect(PixelateEffect()) 53 | 54 | def update(self): 55 | self.sphere.rotate_y(0.01337) 56 | self.rig.update(self.input, self.delta_time) 57 | self.postprocessor.render() 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /examples/5-12-2--postprocessing-color-inversion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.invert import InvertEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate color inversion 21 | """ 22 | def initialize(self): 23 | print("Initializing program...") 24 | self.renderer = Renderer() 25 | self.scene = Scene() 26 | self.camera = Camera(aspect_ratio=800/600) 27 | self.rig = MovementRig() 28 | self.rig.add(self.camera) 29 | self.scene.add(self.rig) 30 | self.rig.set_position([0, 1, 4]) 31 | sky_geometry = SphereGeometry(radius=50) 32 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 33 | sky = Mesh(sky_geometry, sky_material) 34 | self.scene.add(sky) 35 | 36 | grass_geometry = RectangleGeometry(width=100, height=100) 37 | grass_material = TextureMaterial( 38 | texture=Texture(file_name="../py3d/images/grass.jpg"), 39 | property_dict={"repeatUV": [50, 50]} 40 | ) 41 | grass = Mesh(grass_geometry, grass_material) 42 | grass.rotate_x(-math.pi/2) 43 | self.scene.add(grass) 44 | 45 | sphere_geometry = SphereGeometry() 46 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 47 | self.sphere = Mesh(sphere_geometry, sphere_material) 48 | self.sphere.set_position([0, 1, 0]) 49 | self.scene.add(self.sphere) 50 | 51 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 52 | self.postprocessor.add_effect(InvertEffect()) 53 | 54 | def update(self): 55 | self.sphere.rotate_y(0.01337) 56 | self.rig.update(self.input, self.delta_time) 57 | self.postprocessor.render() 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /examples/4-5--rippling-effect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.sphere import SphereGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ 13 | Render an animated rippling effect on a sphere. 14 | The color shifts back and forth from the red end of the spectrum. 15 | """ 16 | def initialize(self): 17 | print("Initializing program...") 18 | self.renderer = Renderer() 19 | self.scene = Scene() 20 | self.camera = Camera(aspect_ratio=800/600) 21 | self.camera.set_position([0, 0, 7]) 22 | geometry = SphereGeometry( 23 | radius=3, 24 | theta_segments=64, 25 | phi_segments=128 26 | ) 27 | vs_code = """ 28 | uniform mat4 modelMatrix; 29 | uniform mat4 viewMatrix; 30 | uniform mat4 projectionMatrix; 31 | in vec3 vertexPosition; 32 | in vec3 vertexColor; 33 | out vec3 color; 34 | uniform float time; 35 | void main() 36 | { 37 | float offset = 0.2 * sin(8.0 * vertexPosition.x + time); 38 | vec3 pos = vertexPosition + vec3(0.0, offset, 0.0); 39 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1); 40 | color = vertexColor; 41 | } 42 | """ 43 | fs_code = """ 44 | in vec3 color; 45 | out vec4 fragColor; 46 | uniform float time; 47 | void main() 48 | { 49 | float r = abs(sin(time)); 50 | vec4 c = vec4(0.25 * r, -0.1 * r, -0.1 * r, 0.0); 51 | fragColor = vec4(color, 1.0) + c; 52 | } 53 | """ 54 | self._time = 0 55 | material = Material(vs_code, fs_code) 56 | material.add_uniform("float", "time", self._time) 57 | material.locate_uniforms() 58 | self.mesh = Mesh(geometry, material) 59 | self.scene.add(self.mesh) 60 | 61 | def update(self): 62 | self.time += 1 / 60 63 | self.mesh.material.uniform_dict["time"].data = self.time 64 | self.renderer.render(self.scene, self.camera) 65 | 66 | 67 | # Instantiate this class and run the program 68 | Example(screen_size=[800, 600]).run() 69 | -------------------------------------------------------------------------------- /py3d/material/texture.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.material.material import Material 4 | 5 | 6 | class TextureMaterial(Material): 7 | def __init__(self, texture, property_dict=None): 8 | vertex_shader_code = """ 9 | uniform mat4 projectionMatrix; 10 | uniform mat4 viewMatrix; 11 | uniform mat4 modelMatrix; 12 | in vec3 vertexPosition; 13 | in vec2 vertexUV; 14 | uniform vec2 repeatUV; 15 | uniform vec2 offsetUV; 16 | out vec2 UV; 17 | void main() 18 | { 19 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); 20 | UV = vertexUV * repeatUV + offsetUV; 21 | } 22 | """ 23 | 24 | fragment_shader_code = """ 25 | uniform vec3 baseColor; 26 | uniform sampler2D textureSampler; 27 | in vec2 UV; 28 | out vec4 fragColor; 29 | void main() 30 | { 31 | vec4 color = vec4(baseColor, 1.0) * texture(textureSampler, UV); 32 | if (color.a < 0.1) 33 | discard; 34 | fragColor = color; 35 | } 36 | """ 37 | super().__init__(vertex_shader_code, fragment_shader_code) 38 | self.add_uniform("vec3", "baseColor", [1.0, 1.0, 1.0]) 39 | self.add_uniform("sampler2D", "textureSampler", [texture.texture_ref, 1]) 40 | self.add_uniform("vec2", "repeatUV", [1.0, 1.0]) 41 | self.add_uniform("vec2", "offsetUV", [0.0, 0.0]) 42 | self.locate_uniforms() 43 | # Render both sides? 44 | self.setting_dict["doubleSide"] = True 45 | # Render triangles as wireframe? 46 | self.setting_dict["wireframe"] = False 47 | # line thickness for wireframe rendering 48 | self.setting_dict["lineWidth"] = 1 49 | self.set_properties(property_dict) 50 | 51 | def update_render_settings(self): 52 | if self.setting_dict["doubleSide"]: 53 | GL.glDisable(GL.GL_CULL_FACE) 54 | else: 55 | GL.glEnable(GL.GL_CULL_FACE) 56 | if self.setting_dict["wireframe"]: 57 | GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_LINE) 58 | else: 59 | GL.glPolygonMode(GL.GL_FRONT_AND_BACK, GL.GL_FILL) 60 | GL.glLineWidth(self.setting_dict["lineWidth"]) 61 | -------------------------------------------------------------------------------- /examples/5-12-4--postprocessing-color-reducing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.color_reduce import ColorReduceEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate reducing colors 21 | """ 22 | def initialize(self): 23 | print("Initializing program...") 24 | self.renderer = Renderer() 25 | self.scene = Scene() 26 | self.camera = Camera(aspect_ratio=800/600) 27 | self.rig = MovementRig() 28 | self.rig.add(self.camera) 29 | self.scene.add(self.rig) 30 | self.rig.set_position([0, 1, 4]) 31 | sky_geometry = SphereGeometry(radius=50) 32 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 33 | sky = Mesh(sky_geometry, sky_material) 34 | self.scene.add(sky) 35 | 36 | grass_geometry = RectangleGeometry(width=100, height=100) 37 | grass_material = TextureMaterial( 38 | texture=Texture(file_name="../py3d/images/grass.jpg"), 39 | property_dict={"repeatUV": [50, 50]} 40 | ) 41 | grass = Mesh(grass_geometry, grass_material) 42 | grass.rotate_x(-math.pi/2) 43 | self.scene.add(grass) 44 | 45 | sphere_geometry = SphereGeometry() 46 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 47 | self.sphere = Mesh(sphere_geometry, sphere_material) 48 | self.sphere.set_position([0, 1, 0]) 49 | self.scene.add(self.sphere) 50 | 51 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 52 | self.postprocessor.add_effect(ColorReduceEffect()) 53 | 54 | def update(self): 55 | self.sphere.rotate_y(0.01337) 56 | self.rig.update(self.input, self.delta_time) 57 | self.postprocessor.render() 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /examples/6-3-1--postprocessing-bright-filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.bright_filter import BrightFilterEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate a bright-filter effect 21 | """ 22 | def initialize(self): 23 | print("Initializing program...") 24 | self.renderer = Renderer() 25 | self.scene = Scene() 26 | self.camera = Camera(aspect_ratio=800/600) 27 | self.rig = MovementRig() 28 | self.rig.add(self.camera) 29 | self.scene.add(self.rig) 30 | self.rig.set_position([0, 1, 4]) 31 | sky_geometry = SphereGeometry(radius=50) 32 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 33 | sky = Mesh(sky_geometry, sky_material) 34 | self.scene.add(sky) 35 | 36 | grass_geometry = RectangleGeometry(width=100, height=100) 37 | grass_material = TextureMaterial( 38 | texture=Texture(file_name="../py3d/images/grass.jpg"), 39 | property_dict={"repeatUV": [50, 50]} 40 | ) 41 | grass = Mesh(grass_geometry, grass_material) 42 | grass.rotate_x(-math.pi/2) 43 | self.scene.add(grass) 44 | 45 | sphere_geometry = SphereGeometry() 46 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 47 | self.sphere = Mesh(sphere_geometry, sphere_material) 48 | self.sphere.set_position([0, 1, 0]) 49 | self.scene.add(self.sphere) 50 | 51 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 52 | self.postprocessor.add_effect(BrightFilterEffect()) 53 | 54 | def update(self): 55 | self.sphere.rotate_y(0.01337) 56 | self.rig.update(self.input, self.delta_time) 57 | self.postprocessor.render() 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /examples/5-12-1--postprocessing-color-tinting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.tint import TintEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate color tinting: predominance of red 21 | """ 22 | def initialize(self): 23 | print("Initializing program...") 24 | self.renderer = Renderer() 25 | self.scene = Scene() 26 | self.camera = Camera(aspect_ratio=800/600) 27 | self.rig = MovementRig() 28 | self.rig.add(self.camera) 29 | self.scene.add(self.rig) 30 | self.rig.set_position([0, 1, 4]) 31 | sky_geometry = SphereGeometry(radius=50) 32 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 33 | sky = Mesh(sky_geometry, sky_material) 34 | self.scene.add(sky) 35 | 36 | grass_geometry = RectangleGeometry(width=100, height=100) 37 | grass_material = TextureMaterial( 38 | texture=Texture(file_name="../py3d/images/grass.jpg"), 39 | property_dict={"repeatUV": [50, 50]} 40 | ) 41 | grass = Mesh(grass_geometry, grass_material) 42 | grass.rotate_x(-math.pi/2) 43 | self.scene.add(grass) 44 | 45 | sphere_geometry = SphereGeometry() 46 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 47 | self.sphere = Mesh(sphere_geometry, sphere_material) 48 | self.sphere.set_position([0, 1, 0]) 49 | self.scene.add(self.sphere) 50 | 51 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 52 | self.postprocessor.add_effect(TintEffect(tint_color=[1, 0, 0])) 53 | 54 | def update(self): 55 | self.sphere.rotate_y(0.01337) 56 | self.rig.update(self.input, self.delta_time) 57 | self.postprocessor.render() 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /examples/5-12-5--postprocessing-vignette.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.vignette import VignetteEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate the vignette effect: reduced brightness towards the edges of an image 21 | """ 22 | def initialize(self): 23 | print("Initializing program...") 24 | self.renderer = Renderer() 25 | self.scene = Scene() 26 | self.camera = Camera(aspect_ratio=800/600) 27 | self.rig = MovementRig() 28 | self.rig.add(self.camera) 29 | self.scene.add(self.rig) 30 | self.rig.set_position([0, 1, 4]) 31 | sky_geometry = SphereGeometry(radius=50) 32 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 33 | sky = Mesh(sky_geometry, sky_material) 34 | self.scene.add(sky) 35 | 36 | grass_geometry = RectangleGeometry(width=100, height=100) 37 | grass_material = TextureMaterial( 38 | texture=Texture(file_name="../py3d/images/grass.jpg"), 39 | property_dict={"repeatUV": [50, 50]} 40 | ) 41 | grass = Mesh(grass_geometry, grass_material) 42 | grass.rotate_x(-math.pi/2) 43 | self.scene.add(grass) 44 | 45 | sphere_geometry = SphereGeometry() 46 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 47 | self.sphere = Mesh(sphere_geometry, sphere_material) 48 | self.sphere.set_position([0, 1, 0]) 49 | self.scene.add(self.sphere) 50 | 51 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 52 | self.postprocessor.add_effect(VignetteEffect()) 53 | 54 | def update(self): 55 | self.sphere.rotate_y(0.01337) 56 | self.rig.update(self.input, self.delta_time) 57 | self.postprocessor.render() 58 | 59 | 60 | # Instantiate this class and run the program 61 | Example(screen_size=[800, 600]).run() 62 | -------------------------------------------------------------------------------- /examples/6-2--bump-mapping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.extras.point_light import PointLightHelper 12 | from py3d.light.ambient import AmbientLight 13 | from py3d.light.point import PointLight 14 | from py3d.material.lambert import LambertMaterial 15 | 16 | 17 | class Example(Base): 18 | """ Bump mapping: combining color textures with normal map textures """ 19 | def initialize(self): 20 | print("Initializing program...") 21 | self.renderer = Renderer() 22 | self.scene = Scene() 23 | self.camera = Camera(aspect_ratio=800/600) 24 | self.camera.set_position([0, 0, 2]) 25 | 26 | ambient_light = AmbientLight(color=[0.3, 0.3, 0.3]) 27 | self.scene.add(ambient_light) 28 | self.point_light = PointLight(color=[1, 1, 1], position=[1, 0, 1]) 29 | self.scene.add(self.point_light) 30 | # texture of a brick wall 31 | color_texture = Texture("../py3d/images/brick-wall.jpg") 32 | # texture of normals of the brick wall 33 | bump_texture = Texture("../py3d/images/brick-wall-normal-map.jpg") 34 | 35 | rectangle_geometry = RectangleGeometry(width=2, height=2) 36 | 37 | color_material = LambertMaterial( 38 | texture=color_texture, 39 | number_of_light_sources=2 40 | ) 41 | 42 | bump_material = LambertMaterial( 43 | texture=color_texture, 44 | bump_texture=bump_texture, 45 | property_dict={"bumpStrength": 1}, 46 | number_of_light_sources=2 47 | ) 48 | 49 | # Replace color_material and bump_material 50 | # in Mesh to see a difference 51 | mesh = Mesh(rectangle_geometry, bump_material) 52 | self.scene.add(mesh) 53 | 54 | point_light_helper = PointLightHelper(self.point_light) 55 | self.point_light.add(point_light_helper) 56 | 57 | def update(self): 58 | self.point_light.set_position([math.cos(0.5 * self.time) / 2, math.sin(0.5 * self.time) / 2, 1]) 59 | self.renderer.render(self.scene, self.camera) 60 | 61 | 62 | # Instantiate this class and run the program 63 | Example(screen_size=[800, 600]).run() 64 | -------------------------------------------------------------------------------- /py3d/material/material.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.core.uniform import Uniform 4 | from py3d.core.utils import Utils 5 | 6 | 7 | class Material: 8 | def __init__(self, vertex_shader_code, fragment_shader_code): 9 | self._program_ref = Utils.initialize_program(vertex_shader_code, fragment_shader_code) 10 | # Store Uniform objects, indexed by name of associated variable in shader. 11 | # Each shader typically contains these uniforms; values will be set during render process from Mesh / Camera. 12 | self._uniform_dict = { 13 | "modelMatrix": Uniform("mat4", None), 14 | "viewMatrix": Uniform("mat4", None), 15 | "projectionMatrix": Uniform("mat4", None), 16 | } 17 | # Store OpenGL render settings, indexed by variable name 18 | self._setting_dict = { 19 | "drawStyle": GL.GL_TRIANGLES 20 | } 21 | 22 | @property 23 | def program_ref(self): 24 | return self._program_ref 25 | 26 | @property 27 | def setting_dict(self): 28 | return self._setting_dict 29 | 30 | @property 31 | def uniform_dict(self): 32 | return self._uniform_dict 33 | 34 | def add_uniform(self, data_type, variable_name, data): 35 | self._uniform_dict[variable_name] = Uniform(data_type, data) 36 | 37 | def locate_uniforms(self): 38 | """ Initialize all uniform variable references """ 39 | for variable_name, uniform_object in self._uniform_dict.items(): 40 | uniform_object.locate_variable(self._program_ref, variable_name) 41 | 42 | def update_render_settings(self): 43 | """ Configure OpenGL with render settings """ 44 | pass 45 | 46 | def set_properties(self, property_dict): 47 | """ 48 | Convenience method for setting multiple material "properties" 49 | (uniform and render setting values) from a dictionary 50 | """ 51 | if property_dict: 52 | for name, data in property_dict.items(): 53 | # Update uniforms 54 | if name in self._uniform_dict.keys(): 55 | self._uniform_dict[name].data = data 56 | # Update render settings 57 | elif name in self._setting_dict.keys(): 58 | self._setting_dict[name] = data 59 | # Unknown property type 60 | else: 61 | raise Exception("Material has no property named: " + name) 62 | -------------------------------------------------------------------------------- /examples/2-05--hexagon-with-vertex-colors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | from py3d.core.attribute import Attribute 7 | 8 | 9 | class Example(Base): 10 | """ Render shapes with vertex colors """ 11 | def initialize(self): 12 | print("Initializing program...") 13 | 14 | # Initialize program # 15 | vs_code = """ 16 | in vec3 position; 17 | in vec3 vertexColor; 18 | out vec3 color; 19 | void main() 20 | { 21 | gl_Position = vec4(position.x, position.y, position.z, 1.0); 22 | color = vertexColor; 23 | } 24 | """ 25 | fs_code = """ 26 | in vec3 color; 27 | out vec4 fragColor; 28 | void main() 29 | { 30 | fragColor = vec4(color.r, color.g, color.b, 1.0); 31 | } 32 | """ 33 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 34 | # render settings (optional) # 35 | GL.glPointSize(10) 36 | GL.glLineWidth(4) 37 | # Set up vertex array object # 38 | vao_ref = GL.glGenVertexArrays(1) 39 | GL.glBindVertexArray(vao_ref) 40 | # Set up vertex attributes # 41 | position_data = [[ 0.8, 0.0, 0.0], 42 | [ 0.4, 0.6, 0.0], 43 | [-0.4, 0.6, 0.0], 44 | [-0.8, 0.0, 0.0], 45 | [-0.4, -0.6, 0.0], 46 | [ 0.4, -0.6, 0.0]] 47 | self.vertex_count = len(position_data) 48 | position_attribute = Attribute("vec3", position_data) 49 | position_attribute.associate_variable(self.program_ref, 'position') 50 | color_data = [[1.0, 0.0, 0.0], 51 | [1.0, 0.5, 0.0], 52 | [1.0, 1.0, 0.0], 53 | [0.0, 1.0, 0.0], 54 | [0.0, 0.0, 1.0], 55 | [0.5, 0.0, 1.0]] 56 | color_attribute = Attribute("vec3", color_data) 57 | color_attribute.associate_variable(self.program_ref, 'vertexColor') 58 | 59 | def update(self): 60 | GL.glUseProgram(self.program_ref) 61 | # GL.glDrawArrays(GL.GL_POINTS, 0, self.vertex_count) 62 | # GL.glDrawArrays(GL.GL_LINE_LOOP, 0, self.vertex_count) 63 | GL.glDrawArrays(GL.GL_TRIANGLE_FAN, 0, self.vertex_count) 64 | 65 | 66 | # Instantiate this class and run the program 67 | Example().run() -------------------------------------------------------------------------------- /examples/6-3-2--postprocessing-blur.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.horizontal_blur import HorizontalBlurEffect 16 | from py3d.effects.vertical_blur import VerticalBlurEffect 17 | 18 | 19 | class Example(Base): 20 | """ 21 | Demonstrate a blur effect 22 | """ 23 | def initialize(self): 24 | print("Initializing program...") 25 | self.renderer = Renderer() 26 | self.scene = Scene() 27 | self.camera = Camera(aspect_ratio=800/600) 28 | self.rig = MovementRig() 29 | self.rig.add(self.camera) 30 | self.scene.add(self.rig) 31 | self.rig.set_position([0, 1, 4]) 32 | sky_geometry = SphereGeometry(radius=50) 33 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 34 | sky = Mesh(sky_geometry, sky_material) 35 | self.scene.add(sky) 36 | 37 | grass_geometry = RectangleGeometry(width=100, height=100) 38 | grass_material = TextureMaterial( 39 | texture=Texture(file_name="../py3d/images/grass.jpg"), 40 | property_dict={"repeatUV": [50, 50]} 41 | ) 42 | grass = Mesh(grass_geometry, grass_material) 43 | grass.rotate_x(-math.pi/2) 44 | self.scene.add(grass) 45 | 46 | sphere_geometry = SphereGeometry() 47 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 48 | self.sphere = Mesh(sphere_geometry, sphere_material) 49 | self.sphere.set_position([0, 1, 0]) 50 | self.scene.add(self.sphere) 51 | 52 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 53 | self.postprocessor.add_effect(HorizontalBlurEffect()) 54 | self.postprocessor.add_effect(VerticalBlurEffect()) 55 | 56 | def update(self): 57 | self.sphere.rotate_y(0.01337) 58 | self.rig.update(self.input, self.delta_time) 59 | self.postprocessor.render() 60 | 61 | 62 | # Instantiate this class and run the program 63 | Example(screen_size=[800, 600]).run() 64 | -------------------------------------------------------------------------------- /examples/5-03--rippling-effect-in-texture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.texture import Texture 8 | from py3d.geometry.rectangle import RectangleGeometry 9 | from py3d.material.material import Material 10 | 11 | 12 | class Example(Base): 13 | """ 14 | Create a rippling effect in a texture, by adding a sine-based displacement 15 | to the V component of the UV coordinates in the fragment shader. 16 | """ 17 | def initialize(self): 18 | print("Initializing program...") 19 | self.renderer = Renderer() 20 | self.scene = Scene() 21 | self.camera = Camera(aspect_ratio=800/600) 22 | self.camera.set_position([0, 0, 1.5]) 23 | vertex_shader_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | in vec2 vertexUV; 29 | out vec2 UV; 30 | 31 | void main() 32 | { 33 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); 34 | UV = vertexUV; 35 | } 36 | """ 37 | fragment_shader_code = """ 38 | uniform sampler2D textureSampler; 39 | in vec2 UV; 40 | uniform float time; 41 | out vec4 fragColor; 42 | 43 | void main() 44 | { 45 | vec2 shiftUV = UV + vec2(0, 0.2 * sin(6.0 * UV.x + time)); 46 | fragColor = texture(textureSampler, shiftUV); 47 | } 48 | """ 49 | grid_texure = Texture("../py3d/images/grid.jpg") 50 | self.wave_material = Material(vertex_shader_code, fragment_shader_code) 51 | self.wave_material.add_uniform("sampler2D", "textureSampler", [grid_texure.texture_ref, 1]) 52 | self.wave_material.add_uniform("float", "time", 0.0) 53 | self.wave_material.locate_uniforms() 54 | 55 | geometry = RectangleGeometry() 56 | self.mesh = Mesh(geometry, self.wave_material) 57 | self.scene.add(self.mesh) 58 | 59 | def update(self): 60 | self.wave_material.uniform_dict["time"].data += self.delta_time 61 | self.renderer.render(self.scene, self.camera) 62 | 63 | 64 | # Instantiate this class and run the program 65 | Example(screen_size=[800, 600]).run() 66 | -------------------------------------------------------------------------------- /examples/2-04--shapes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | from py3d.core.attribute import Attribute 7 | 8 | 9 | class Example(Base): 10 | """ Render two shapes """ 11 | def initialize(self): 12 | print("Initializing program...") 13 | # Initialize program # 14 | vs_code = """ 15 | in vec3 position; 16 | void main() 17 | { 18 | gl_Position = vec4(position.x, position.y, position.z, 1.0); 19 | } 20 | """ 21 | fs_code = """ 22 | out vec4 fragColor; 23 | void main() 24 | { 25 | fragColor = vec4(1.0, 1.0, 0.0, 1.0); 26 | } 27 | """ 28 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 29 | # render settings # 30 | GL.glLineWidth(4) 31 | # Set up vertex array object - triangle # 32 | self.vao_triangle = GL.glGenVertexArrays(1) 33 | GL.glBindVertexArray(self.vao_triangle) 34 | position_data_triangle = [[-0.5, 0.8, 0.0], 35 | [-0.2, 0.2, 0.0], 36 | [-0.8, 0.2, 0.0]] 37 | self.vertex_count_triangle = len(position_data_triangle) 38 | position_attribute_triangle = Attribute('vec3', position_data_triangle) 39 | position_attribute_triangle.associate_variable(self.program_ref, 'position') 40 | # Set up vertex array object - square # 41 | self.vao_square = GL.glGenVertexArrays(1) 42 | GL.glBindVertexArray(self.vao_square) 43 | position_data_square = [[0.8, 0.8, 0.0], 44 | [0.8, 0.2, 0.0], 45 | [0.2, 0.2, 0.0], 46 | [0.2, 0.8, 0.0]] 47 | self.vertex_count_square = len(position_data_square) 48 | position_attribute_square = Attribute('vec3', position_data_square) 49 | position_attribute_square.associate_variable(self.program_ref, 'position') 50 | 51 | def update(self): 52 | # Using same program to render both shapes 53 | GL.glUseProgram(self.program_ref) 54 | # Draw the triangle 55 | GL.glBindVertexArray(self.vao_triangle) 56 | GL.glDrawArrays(GL.GL_LINE_LOOP, 0, self.vertex_count_triangle) 57 | # Draw the square 58 | GL.glBindVertexArray(self.vao_square) 59 | GL.glDrawArrays(GL.GL_LINE_LOOP, 0, self.vertex_count_square) 60 | 61 | 62 | # Instantiate this class and run the program 63 | Example().run() -------------------------------------------------------------------------------- /examples/2-08--animation-2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | import OpenGL.GL as GL 5 | 6 | from py3d.core.base import Base 7 | from py3d.core.utils import Utils 8 | from py3d.core.attribute import Attribute 9 | from py3d.core.uniform import Uniform 10 | 11 | 12 | class Example(Base): 13 | """ Animate a triangle moving in a circular path around the origin """ 14 | def initialize(self): 15 | print("Initializing program...") 16 | # Initialize program # 17 | vs_code = """ 18 | in vec3 position; 19 | uniform vec3 translation; 20 | void main() 21 | { 22 | vec3 pos = position + translation; 23 | gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); 24 | } 25 | """ 26 | fs_code = """ 27 | uniform vec3 baseColor; 28 | out vec4 fragColor; 29 | void main() 30 | { 31 | fragColor = vec4(baseColor.r, baseColor.g, baseColor.b, 1.0); 32 | } 33 | """ 34 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 35 | # Render settings (optional) # 36 | # Specify color used when clearly 37 | GL.glClearColor(0.0, 0.0, 0.0, 1.0) 38 | # Set up vertex array object # 39 | vao_ref = GL.glGenVertexArrays(1) 40 | GL.glBindVertexArray(vao_ref) 41 | # Set up vertex attribute # 42 | position_data = [[ 0.0, 0.2, 0.0], 43 | [ 0.2, -0.2, 0.0], 44 | [-0.2, -0.2, 0.0]] 45 | self.vertex_count = len(position_data) 46 | position_attribute = Attribute('vec3', position_data) 47 | position_attribute.associate_variable(self.program_ref, 'position') 48 | # Set up uniforms # 49 | self.translation = Uniform('vec3', [-0.5, 0.0, 0.0]) 50 | self.translation.locate_variable(self.program_ref, 'translation') 51 | self.base_color = Uniform('vec3', [1.0, 0.0, 0.0]) 52 | self.base_color.locate_variable(self.program_ref, 'baseColor') 53 | 54 | def update(self): 55 | """ Update data """ 56 | self.translation.data[0] = 0.75 * math.cos(self.time) 57 | self.translation.data[1] = 0.75 * math.sin(self.time) 58 | # Reset color buffer with specified color 59 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 60 | GL.glUseProgram(self.program_ref) 61 | self.translation.upload_data() 62 | self.base_color.upload_data() 63 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 64 | 65 | 66 | # Instantiate this class and run the program 67 | Example().run() -------------------------------------------------------------------------------- /py3d/extras/text_texture.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from py3d.core_ext.texture import Texture 4 | 5 | class TextTexture(Texture): 6 | """ 7 | Define a text texture by using pygame 8 | """ 9 | def __init__(self, text="Python graphics", 10 | system_font_name="Arial", 11 | font_file_name=None, 12 | font_size=24, 13 | font_color=(0, 0, 0), 14 | background_color=(255, 255, 255), 15 | transparent=False, 16 | image_width=None, 17 | image_height=None, 18 | align_horizontal=0.0, 19 | align_vertical=0.0, 20 | image_border_width=0, 21 | image_border_color=(0, 0, 0)): 22 | super().__init__() 23 | # Set a default font 24 | font = pygame.font.SysFont(system_font_name, font_size) 25 | # The font can be overrided by loading font file 26 | if font_file_name is not None: 27 | font = pygame.font.Font(font_file_name, font_size) 28 | # Render text to (antialiased) surface 29 | font_surface = font.render(text, True, font_color) 30 | # Determine size of rendered text for alignment purposes 31 | (text_width, text_height) = font.size(text) 32 | # If image dimensions are not specified, 33 | # use the font surface size as default 34 | if image_width is None: 35 | image_width = text_width 36 | if image_height is None: 37 | image_height = text_height 38 | # Create a surface to store the image of text 39 | # (with the transparency channel by default) 40 | self._surface = pygame.Surface((image_width, image_height), 41 | pygame.SRCALPHA) 42 | # Set a background color used when not transparent 43 | if not transparent: 44 | self._surface.fill(background_color) 45 | # Attributes align_horizontal, align_vertical define percentages, 46 | # measured from top-left corner 47 | corner_point = (align_horizontal * (image_width - text_width), 48 | align_vertical * (image_height - text_height)) 49 | destination_rectangle = font_surface.get_rect(topleft=corner_point) 50 | # Add border (optionally) 51 | if image_border_width > 0: 52 | pygame.draw.rect(self._surface, image_border_color, 53 | [0, 0, image_width, image_height], image_border_width) 54 | # Apply font_surface to a correct position on the final surface 55 | self._surface.blit(font_surface, destination_rectangle) 56 | self.upload_data() 57 | 58 | -------------------------------------------------------------------------------- /py3d/core/attribute.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | import numpy as np 3 | 4 | 5 | class Attribute: 6 | def __init__(self, data_type, data): 7 | # type of elements in data array: int | float | vec2 | vec3 | vec4 8 | self._data_type = data_type 9 | # array of data to be stored in buffer 10 | self._data = data 11 | # reference of available buffer from GPU 12 | self._buffer_ref = GL.glGenBuffers(1) 13 | # Upload data immediately 14 | self.upload_data() 15 | 16 | @property 17 | def data(self): 18 | return self._data 19 | 20 | @data.setter 21 | def data(self, data): 22 | self._data = data 23 | 24 | def upload_data(self): 25 | """ Upload the data to a GPU buffer """ 26 | # Convert data to numpy array format; convert numbers to 32-bit floats 27 | data = np.array(self._data).astype(np.float32) 28 | # Select buffer used by the following functions 29 | GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._buffer_ref) 30 | # Store data in currently bound buffer 31 | GL.glBufferData(GL.GL_ARRAY_BUFFER, data.ravel(), GL.GL_STATIC_DRAW) 32 | 33 | def associate_variable(self, program_ref, variable_name): 34 | """ Associate variable in program with the buffer """ 35 | # Get reference for program variable with given name 36 | variable_ref = GL.glGetAttribLocation(program_ref, variable_name) 37 | # If the program does not reference the variable, then exit 38 | if variable_ref != -1: 39 | # Select buffer used by the following functions 40 | GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._buffer_ref) 41 | # Specify how data will be read from the currently bound buffer into the specified variable 42 | if self._data_type == "int": 43 | GL.glVertexAttribPointer(variable_ref, 1, GL.GL_INT, False, 0, None) 44 | elif self._data_type == "float": 45 | GL.glVertexAttribPointer(variable_ref, 1, GL.GL_FLOAT, False, 0, None) 46 | elif self._data_type == "vec2": 47 | GL.glVertexAttribPointer(variable_ref, 2, GL.GL_FLOAT, False, 0, None) 48 | elif self._data_type == "vec3": 49 | GL.glVertexAttribPointer(variable_ref, 3, GL.GL_FLOAT, False, 0, None) 50 | elif self._data_type == "vec4": 51 | GL.glVertexAttribPointer(variable_ref, 4, GL.GL_FLOAT, False, 0, None) 52 | else: 53 | raise Exception(f'Attribute {variable_name} has unknown type {self._data_type}') 54 | # Indicate that data will be streamed to this variable 55 | GL.glEnableVertexAttribArray(variable_ref) 56 | -------------------------------------------------------------------------------- /examples/2-06--two-triangles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | from py3d.core.attribute import Attribute 7 | from py3d.core.uniform import Uniform 8 | 9 | 10 | class Example(Base): 11 | """ Render two triangles with different positions and colors """ 12 | def initialize(self): 13 | print("Initializing program...") 14 | # Initialize program # 15 | vs_code = """ 16 | in vec3 position; 17 | uniform vec3 translation; 18 | void main() 19 | { 20 | vec3 pos = position + translation; 21 | gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); 22 | } 23 | """ 24 | fs_code = """ 25 | uniform vec3 baseColor; 26 | out vec4 fragColor; 27 | void main() 28 | { 29 | fragColor = vec4(baseColor.r, baseColor.g, baseColor.b, 1.0); 30 | } 31 | """ 32 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 33 | # Set up vertex array object # 34 | vao_ref = GL.glGenVertexArrays(1) 35 | GL.glBindVertexArray(vao_ref) 36 | # Set up vertex attribute # 37 | position_data = [[ 0.0, 0.2, 0.0], 38 | [ 0.2, -0.2, 0.0], 39 | [-0.2, -0.2, 0.0]] 40 | self.vertex_count = len(position_data) 41 | position_attribute = Attribute('vec3', position_data) 42 | position_attribute.associate_variable(self.program_ref, 'position') 43 | # Set up uniforms # 44 | self.translation1 = Uniform('vec3', [-0.5, 0.0, 0.0]) 45 | self.translation1.locate_variable(self.program_ref, 'translation') 46 | self.translation2 = Uniform('vec3', [0.5, 0.0, 0.0]) 47 | self.translation2.locate_variable(self.program_ref, 'translation') 48 | self.base_color1 = Uniform('vec3', [1.0, 0.0, 0.0]) 49 | self.base_color1.locate_variable(self.program_ref, 'baseColor') 50 | self.base_color2 = Uniform('vec3', [0.0, 0.0, 1.0]) 51 | self.base_color2.locate_variable(self.program_ref, 'baseColor') 52 | 53 | def update(self): 54 | GL.glUseProgram(self.program_ref) 55 | # Draw the first triangle 56 | self.translation1.upload_data() 57 | self.base_color1.upload_data() 58 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 59 | # Draw the second triangle 60 | self.translation2.upload_data() 61 | self.base_color2.upload_data() 62 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 63 | 64 | 65 | # Instantiate this class and run the program 66 | Example().run() 67 | -------------------------------------------------------------------------------- /examples/5-08--billboarding-by-look-at.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.matrix import Matrix 6 | from py3d.core_ext.camera import Camera 7 | from py3d.core_ext.mesh import Mesh 8 | from py3d.core_ext.renderer import Renderer 9 | from py3d.core_ext.scene import Scene 10 | from py3d.core_ext.texture import Texture 11 | from py3d.geometry.box import BoxGeometry 12 | from py3d.geometry.rectangle import RectangleGeometry 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.text_texture import TextTexture 15 | from py3d.material.texture import TextureMaterial 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate billboarding by using look-at. 21 | A billboard always looks at a moving camera. 22 | Use keys ADRF to move the camera and see this effect. 23 | """ 24 | def initialize(self): 25 | print("Initializing program...") 26 | self.renderer = Renderer() 27 | self.scene = Scene() 28 | self.camera = Camera(aspect_ratio=800/600) 29 | self.rig = MovementRig() 30 | self.rig.add(self.camera) 31 | self.rig.set_position([0, 1, 5]) 32 | self.scene.add(self.rig) 33 | 34 | label_texture = TextTexture(text=" This is a Crate ", 35 | system_font_name="Arial Bold", 36 | font_size=40, 37 | font_color=[0, 0, 200], 38 | image_width=256, 39 | image_height=128, 40 | align_horizontal=0.5, 41 | align_vertical=0.5, 42 | image_border_width=4, 43 | image_border_color=[255, 0, 0]) 44 | label_material = TextureMaterial(label_texture) 45 | label_geometry = RectangleGeometry(width=1, height=0.5) 46 | label_geometry.apply_matrix(Matrix.make_rotation_y(math.pi)) 47 | self.label = Mesh(label_geometry, label_material) 48 | self.label.set_position([0, 1, 0]) 49 | self.scene.add(self.label) 50 | 51 | crate_geometry = BoxGeometry() 52 | crate_texture = Texture("../py3d/images/crate.jpg") 53 | crate_material = TextureMaterial(crate_texture) 54 | crate = Mesh(crate_geometry, crate_material) 55 | self.scene.add(crate) 56 | 57 | def update(self): 58 | self.rig.update(self.input, self.delta_time) 59 | self.label.look_at(self.camera.global_position) 60 | self.renderer.render(self.scene, self.camera) 61 | 62 | 63 | # Instantiate this class and run the program 64 | Example(screen_size=[800, 600]).run() 65 | -------------------------------------------------------------------------------- /py3d/extras/postprocessor.py: -------------------------------------------------------------------------------- 1 | from py3d.core_ext.renderer import Renderer 2 | from py3d.core_ext.scene import Scene 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.render_target import RenderTarget 6 | from py3d.geometry.geometry import Geometry 7 | 8 | 9 | class Postprocessor: 10 | """ 11 | Create effects by postprocessing 12 | """ 13 | def __init__(self, 14 | renderer: Renderer, 15 | scene: Scene, 16 | camera: Camera, 17 | final_render_target=None): 18 | self._renderer = renderer 19 | self._scene_list = [scene] 20 | self._camera_list = [camera] 21 | self._render_target_list = [final_render_target] 22 | self._final_render_target = final_render_target 23 | self._ortho_camera = Camera() 24 | self._ortho_camera.set_orthographic() # aligned with clip space 25 | # By default, generate a rectangle already aligned with clip space; 26 | # no matrix transformations will be applied 27 | self._rectangle_geometry = Geometry() 28 | p0, p1, p2, p3 = [-1, -1], [1, -1], [-1, 1], [1, 1] 29 | t0, t1, t2, t3 = [0, 0], [1, 0], [0, 1], [1, 1] 30 | position_data = [p0, p1, p3, p0, p3, p2] 31 | uv_data = [t0, t1, t3, t0, t3, t2] 32 | self._rectangle_geometry.add_attribute("vec2", "vertexPosition", position_data) 33 | self._rectangle_geometry.add_attribute("vec2", "vertexUV", uv_data) 34 | 35 | @property 36 | def render_target_list(self): 37 | return self._render_target_list 38 | 39 | def add_effect(self, effect): 40 | post_scene = Scene() 41 | resolution = self._renderer.window_size 42 | target = RenderTarget(resolution=resolution) 43 | # Change the previous entry in the render target list 44 | # to this newly created render target 45 | self._render_target_list[-1] = target 46 | # The effect in this render pass will use the texture 47 | # that was written to in the previous render pass 48 | effect.uniform_dict["textureSampler"].data[0] = target.texture.texture_ref 49 | mesh = Mesh(self._rectangle_geometry, effect) 50 | post_scene.add(mesh) 51 | self._scene_list.append(post_scene) 52 | self._camera_list.append(self._ortho_camera) 53 | self._render_target_list.append(self._final_render_target) 54 | 55 | def render(self): 56 | passes = len(self._scene_list) 57 | for n in range(passes): 58 | scene = self._scene_list[n] 59 | camera = self._camera_list[n] 60 | target = self._render_target_list[n] 61 | self._renderer.render(scene, camera, render_target=target) 62 | -------------------------------------------------------------------------------- /examples/2-09--color-changing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | import OpenGL.GL as GL 5 | 6 | from py3d.core.base import Base 7 | from py3d.core.utils import Utils 8 | from py3d.core.attribute import Attribute 9 | from py3d.core.uniform import Uniform 10 | 11 | 12 | class Example(Base): 13 | """ Animate a triangle changing its color """ 14 | def initialize(self): 15 | print("Initializing program...") 16 | # Initialize program # 17 | vs_code = """ 18 | in vec3 position; 19 | uniform vec3 translation; 20 | void main() 21 | { 22 | vec3 pos = position + translation; 23 | gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); 24 | } 25 | """ 26 | fs_code = """ 27 | uniform vec3 baseColor; 28 | out vec4 fragColor; 29 | void main() 30 | { 31 | fragColor = vec4(baseColor.r, baseColor.g, baseColor.b, 1.0); 32 | } 33 | """ 34 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 35 | # render settings (optional) # 36 | # Specify color used when clearly 37 | GL.glClearColor(0.0, 0.0, 0.0, 1.0) 38 | # Set up vertex array object # 39 | vao_ref = GL.glGenVertexArrays(1) 40 | GL.glBindVertexArray(vao_ref) 41 | # Set up vertex attribute # 42 | position_data = [[ 0.0, 0.2, 0.0], 43 | [ 0.2, -0.2, 0.0], 44 | [-0.2, -0.2, 0.0]] 45 | self.vertex_count = len(position_data) 46 | position_attribute = Attribute('vec3', position_data) 47 | position_attribute.associate_variable(self.program_ref, 'position') 48 | # Set up uniforms # 49 | self.translation = Uniform('vec3', [-0.5, 0.0, 0.0]) 50 | self.translation.locate_variable(self.program_ref, 'translation') 51 | self.base_color = Uniform('vec3', [1.0, 0.0, 0.0]) 52 | self.base_color.locate_variable(self.program_ref, 'baseColor') 53 | 54 | def update(self): 55 | """ Update data """ 56 | # self.base_color.data[0] = (math.sin(3 * self.time) + 1) / 2 57 | self.base_color.data[0] = (math.sin(self.time) + 1) / 2 58 | self.base_color.data[1] = (math.sin(self.time + 2.1) + 1) / 2 59 | self.base_color.data[2] = (math.sin(self.time + 4.2) + 1) / 2 60 | # Reset color buffer with specified color 61 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 62 | GL.glUseProgram(self.program_ref) 63 | self.translation.upload_data() 64 | self.base_color.upload_data() 65 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 66 | 67 | 68 | # Instantiate this class and run the program 69 | Example().run() -------------------------------------------------------------------------------- /examples/5-12-6--postprocessing-compound-effect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.color_reduce import ColorReduceEffect 16 | from py3d.effects.tint import TintEffect 17 | from py3d.effects.pixelate import PixelateEffect 18 | 19 | 20 | class Example(Base): 21 | """ 22 | Demonstrate a compound effect consisting of subsequent 23 | tint, color reduce, and pixelate effects 24 | """ 25 | def initialize(self): 26 | print("Initializing program...") 27 | self.renderer = Renderer() 28 | self.scene = Scene() 29 | self.camera = Camera(aspect_ratio=800/600) 30 | self.rig = MovementRig() 31 | self.rig.add(self.camera) 32 | self.scene.add(self.rig) 33 | self.rig.set_position([0, 1, 4]) 34 | sky_geometry = SphereGeometry(radius=50) 35 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 36 | sky = Mesh(sky_geometry, sky_material) 37 | self.scene.add(sky) 38 | 39 | grass_geometry = RectangleGeometry(width=100, height=100) 40 | grass_material = TextureMaterial( 41 | texture=Texture(file_name="../py3d/images/grass.jpg"), 42 | property_dict={"repeatUV": [50, 50]} 43 | ) 44 | grass = Mesh(grass_geometry, grass_material) 45 | grass.rotate_x(-math.pi/2) 46 | self.scene.add(grass) 47 | 48 | sphere_geometry = SphereGeometry() 49 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 50 | self.sphere = Mesh(sphere_geometry, sphere_material) 51 | self.sphere.set_position([0, 1, 0]) 52 | self.scene.add(self.sphere) 53 | 54 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 55 | self.postprocessor.add_effect(TintEffect(tint_color=[0, 1, 0])) 56 | self.postprocessor.add_effect(ColorReduceEffect(levels=5)) 57 | self.postprocessor.add_effect(PixelateEffect(resolution=[800, 600])) 58 | 59 | def update(self): 60 | self.sphere.rotate_y(0.01337) 61 | self.rig.update(self.input, self.delta_time) 62 | self.postprocessor.render() 63 | 64 | 65 | # Instantiate this class and run the program 66 | Example(screen_size=[800, 600]).run() 67 | -------------------------------------------------------------------------------- /py3d/core/base.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | 4 | from py3d.core.input import Input 5 | from py3d.core.utils import Utils 6 | 7 | 8 | class Base: 9 | def __init__(self, screen_size=(512, 512)): 10 | # Initialize all pygame modules 11 | pygame.init() 12 | # Indicate rendering details 13 | display_flags = pygame.DOUBLEBUF | pygame.OPENGL 14 | # Initialize buffers to perform antialiasing 15 | pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1) 16 | pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES, 4) 17 | # Use a core OpenGL profile for cross-platform compatibility 18 | pygame.display.gl_set_attribute(pygame.GL_CONTEXT_PROFILE_MASK, pygame.GL_CONTEXT_PROFILE_CORE) 19 | # Create and display the window 20 | self._screen = pygame.display.set_mode(screen_size, display_flags) 21 | # Set the text that appears in the title bar of the window 22 | pygame.display.set_caption("Graphics Window") 23 | # Determine if main loop is active 24 | self._running = True 25 | # Manage time-related data and operations 26 | self._clock = pygame.time.Clock() 27 | # Manage user input 28 | self._input = Input() 29 | # number of seconds application has been running 30 | self._time = 0 31 | # Print the system information 32 | Utils.print_system_info() 33 | 34 | @property 35 | def delta_time(self): 36 | return self._delta_time 37 | 38 | @property 39 | def input(self): 40 | return self._input 41 | 42 | @property 43 | def time(self): 44 | return self._time 45 | 46 | @time.setter 47 | def time(self, value): 48 | self._time = value 49 | 50 | def initialize(self): 51 | """ Implement by extending class """ 52 | pass 53 | 54 | def update(self): 55 | """ Implement by extending class """ 56 | pass 57 | 58 | def run(self): 59 | # Startup # 60 | self.initialize() 61 | # main loop # 62 | while self._running: 63 | # process input # 64 | self._input.update() 65 | if self._input.quit: 66 | self._running = False 67 | # seconds since iteration of run loop 68 | self._delta_time = self._clock.get_time() / 1000 69 | # Increment time application has been running 70 | self._time += self._delta_time 71 | # Update # 72 | self.update() 73 | # Render # 74 | # Display image on screen 75 | pygame.display.flip() 76 | # Pause if necessary to achieve 60 FPS 77 | self._clock.tick(60) 78 | # Shutdown # 79 | pygame.quit() 80 | sys.exit() 81 | -------------------------------------------------------------------------------- /examples/2-07--animation-1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | from py3d.core.attribute import Attribute 7 | from py3d.core.uniform import Uniform 8 | 9 | 10 | class Example(Base): 11 | """ Animate a triangle moving across screen """ 12 | def initialize(self): 13 | print("Initializing program...") 14 | # Initialize program # 15 | vs_code = """ 16 | in vec3 position; 17 | uniform vec3 translation; 18 | void main() 19 | { 20 | vec3 pos = position + translation; 21 | gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); 22 | } 23 | """ 24 | fs_code = """ 25 | uniform vec3 baseColor; 26 | out vec4 fragColor; 27 | void main() 28 | { 29 | fragColor = vec4(baseColor.r, baseColor.g, baseColor.b, 1.0); 30 | } 31 | """ 32 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 33 | # render settings (optional) # 34 | # Specify color used when clearly 35 | GL.glClearColor(0.0, 0.0, 0.0, 1.0) 36 | # Set up vertex array object # 37 | vao_ref = GL.glGenVertexArrays(1) 38 | GL.glBindVertexArray(vao_ref) 39 | # Set up vertex attribute # 40 | position_data = [[ 0.0, 0.2, 0.0], 41 | [ 0.2, -0.2, 0.0], 42 | [-0.2, -0.2, 0.0]] 43 | self.vertex_count = len(position_data) 44 | position_attribute = Attribute('vec3', position_data) 45 | position_attribute.associate_variable(self.program_ref, 'position') 46 | # Set up uniforms # 47 | self.translation = Uniform('vec3', [-0.5, 0.0, 0.0]) 48 | self.translation.locate_variable(self.program_ref, 'translation') 49 | self.base_color = Uniform('vec3', [1.0, 0.0, 0.0]) 50 | self.base_color.locate_variable(self.program_ref, 'baseColor') 51 | 52 | def update(self): 53 | """ Update data """ 54 | # Increase x coordinate of translation 55 | self.translation.data[0] += 0.01 56 | # If triangle passes off-screen on the right, 57 | # change translation, so it reappears on the left 58 | if self.translation.data[0] > 1.2: 59 | self.translation.data[0] = -1.2 60 | # Render scene # 61 | # Reset color buffer with specified color 62 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 63 | GL.glUseProgram(self.program_ref) 64 | self.translation.upload_data() 65 | self.base_color.upload_data() 66 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 67 | 68 | 69 | # Instantiate this class and run the program 70 | Example().run() 71 | -------------------------------------------------------------------------------- /examples/6-3-3--postprocessing-additive-blend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.additive_blend import AdditiveBlendEffect 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate an additive blend effect. 21 | Note that the top part of the scene contains sky colors, 22 | and the bottom part contains grass colors. 23 | Therefore, the top part is brighter, and the bottom part is darker. 24 | """ 25 | def initialize(self): 26 | print("Initializing program...") 27 | self.renderer = Renderer() 28 | self.scene = Scene() 29 | self.camera = Camera(aspect_ratio=800/600) 30 | self.rig = MovementRig() 31 | self.rig.add(self.camera) 32 | self.scene.add(self.rig) 33 | self.rig.set_position([0, 1, 4]) 34 | sky_geometry = SphereGeometry(radius=50) 35 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 36 | sky = Mesh(sky_geometry, sky_material) 37 | self.scene.add(sky) 38 | 39 | grass_geometry = RectangleGeometry(width=100, height=100) 40 | grass_material = TextureMaterial( 41 | texture=Texture(file_name="../py3d/images/grass.jpg"), 42 | property_dict={"repeatUV": [50, 50]} 43 | ) 44 | grass = Mesh(grass_geometry, grass_material) 45 | grass.rotate_x(-math.pi/2) 46 | self.scene.add(grass) 47 | 48 | sphere_geometry = SphereGeometry() 49 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 50 | self.sphere = Mesh(sphere_geometry, sphere_material) 51 | self.sphere.set_position([0, 1, 0]) 52 | self.scene.add(self.sphere) 53 | 54 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 55 | self.postprocessor.add_effect( 56 | AdditiveBlendEffect( 57 | blend_texture=Texture("../py3d/images/crate.jpg"), 58 | original_strength=1, 59 | blend_strength=1 60 | ) 61 | ) 62 | 63 | def update(self): 64 | self.sphere.rotate_y(0.01337) 65 | self.rig.update(self.input, self.delta_time) 66 | self.postprocessor.render() 67 | 68 | 69 | # Instantiate this class and run the program 70 | Example(screen_size=[800, 600]).run() 71 | -------------------------------------------------------------------------------- /examples/5-05--distorting-texture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.texture import Texture 8 | from py3d.geometry.rectangle import RectangleGeometry 9 | from py3d.material.material import Material 10 | 11 | 12 | class Example(Base): 13 | """ Distort a texture over time by using a RGB noise texture with pseudo-random uv-coordinates """ 14 | def initialize(self): 15 | print("Initializing program...") 16 | self.renderer = Renderer() 17 | self.scene = Scene() 18 | self.camera = Camera(aspect_ratio=800/600) 19 | self.camera.set_position([0, 0, 1.5]) 20 | vertex_shader_code = """ 21 | uniform mat4 projectionMatrix; 22 | uniform mat4 viewMatrix; 23 | uniform mat4 modelMatrix; 24 | in vec3 vertexPosition; 25 | in vec2 vertexUV; 26 | out vec2 UV; 27 | 28 | void main() 29 | { 30 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); 31 | UV = vertexUV; 32 | } 33 | """ 34 | fragment_shader_code = """ 35 | uniform sampler2D rgbNoiseSampler; 36 | uniform sampler2D imageSampler; 37 | in vec2 UV; 38 | uniform float time; 39 | out vec4 fragColor; 40 | 41 | void main() 42 | { 43 | vec2 uvShift = UV + vec2(-0.033, 0.07) * time; 44 | vec4 noiseValues = texture(rgbNoiseSampler, uvShift); 45 | vec2 uvNoise = UV + 0.01 * noiseValues.rg; 46 | fragColor = texture(imageSampler, uvNoise); 47 | } 48 | """ 49 | rgb_noise_texture = Texture("../py3d/images/rgb-noise.jpg") 50 | grid_texture = Texture("../py3d/images/grid.jpg") 51 | self.distort_material = Material(vertex_shader_code, fragment_shader_code) 52 | self.distort_material.add_uniform("sampler2D", "rgbNoiseSampler", [rgb_noise_texture.texture_ref, 1]) 53 | self.distort_material.add_uniform("sampler2D", "imageSampler", [grid_texture.texture_ref, 2]) 54 | self.distort_material.add_uniform("float", "time", 0.0) 55 | self.distort_material.locate_uniforms() 56 | 57 | geometry = RectangleGeometry() 58 | self.mesh = Mesh(geometry, self.distort_material) 59 | self.scene.add(self.mesh) 60 | 61 | def update(self): 62 | self.distort_material.uniform_dict["time"].data += self.delta_time 63 | self.renderer.render(self.scene, self.camera) 64 | 65 | 66 | # Instantiate this class and run the program 67 | Example(screen_size=[800, 600]).run() 68 | -------------------------------------------------------------------------------- /examples/5-04--blending-between-textures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.texture import Texture 8 | from py3d.geometry.rectangle import RectangleGeometry 9 | from py3d.material.material import Material 10 | 11 | 12 | class Example(Base): 13 | """ 14 | Blend between two different textures cyclically. The fragment shader 15 | samples colors from two textures at each fragment, and then, linearly 16 | interpolates between these colors to determine the output fragment color. 17 | """ 18 | def initialize(self): 19 | print("Initializing program...") 20 | self.renderer = Renderer() 21 | self.scene = Scene() 22 | self.camera = Camera(aspect_ratio=800/600) 23 | self.camera.set_position([0, 0, 1.5]) 24 | vertex_shader_code = """ 25 | uniform mat4 projectionMatrix; 26 | uniform mat4 viewMatrix; 27 | uniform mat4 modelMatrix; 28 | in vec3 vertexPosition; 29 | in vec2 vertexUV; 30 | out vec2 UV; 31 | 32 | void main() 33 | { 34 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); 35 | UV = vertexUV; 36 | } 37 | """ 38 | fragment_shader_code = """ 39 | uniform sampler2D textureSampler1; 40 | uniform sampler2D textureSampler2; 41 | in vec2 UV; 42 | uniform float time; 43 | out vec4 fragColor; 44 | 45 | void main() 46 | { 47 | vec4 color1 = texture(textureSampler1, UV); 48 | vec4 color2 = texture(textureSampler2, UV); 49 | float s = abs(sin(time)); 50 | fragColor = s * color1 + (1.0 - s) * color2; 51 | } 52 | """ 53 | grid_texture = Texture("../py3d/images/grid.jpg") 54 | crate_texture = Texture("../py3d/images/crate.jpg") 55 | self.blend_material = Material(vertex_shader_code, fragment_shader_code) 56 | self.blend_material.add_uniform("sampler2D", "textureSampler1", [grid_texture.texture_ref, 1]) 57 | self.blend_material.add_uniform("sampler2D", "textureSampler2", [crate_texture.texture_ref, 2]) 58 | self.blend_material.add_uniform("float", "time", 0.0) 59 | self.blend_material.locate_uniforms() 60 | 61 | geometry = RectangleGeometry() 62 | self.mesh = Mesh(geometry, self.blend_material) 63 | self.scene.add(self.mesh) 64 | 65 | def update(self): 66 | self.blend_material.uniform_dict["time"].data += self.delta_time 67 | self.renderer.render(self.scene, self.camera) 68 | 69 | 70 | # Instantiate this class and run the program 71 | Example(screen_size=[800, 600]).run() 72 | -------------------------------------------------------------------------------- /py3d/extras/movement_rig.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from py3d.core_ext.object3d import Object3D 4 | 5 | 6 | class MovementRig(Object3D): 7 | """ 8 | Add moving forwards and backwards, left and right, up and down (all local translations), 9 | as well as turning left and right, and looking up and down 10 | """ 11 | def __init__(self, units_per_second=1, degrees_per_second=60): 12 | # Initialize base Object3D. 13 | # Controls movement and turn left/right. 14 | super().__init__() 15 | # Initialize attached Object3D; controls look up/down 16 | self._look_attachment = Object3D() 17 | self.children_list = [self._look_attachment] 18 | self._look_attachment.parent = self 19 | # Control rate of movement 20 | self._units_per_second = units_per_second 21 | self._degrees_per_second = degrees_per_second 22 | 23 | # Customizable key mappings. 24 | # Defaults: W, A, S, D, R, F (move), Q, E (turn), T, G (look) 25 | self.KEY_MOVE_FORWARDS = "w" 26 | self.KEY_MOVE_BACKWARDS = "s" 27 | self.KEY_MOVE_LEFT = "a" 28 | self.KEY_MOVE_RIGHT = "d" 29 | self.KEY_MOVE_UP = "r" 30 | self.KEY_MOVE_DOWN = "f" 31 | self.KEY_TURN_LEFT = "q" 32 | self.KEY_TURN_RIGHT = "e" 33 | self.KEY_LOOK_UP = "t" 34 | self.KEY_LOOK_DOWN = "g" 35 | 36 | # Adding and removing objects applies to look attachment. 37 | # Override functions from the Object3D class. 38 | def add(self, child): 39 | self._look_attachment.add(child) 40 | 41 | def remove(self, child): 42 | self._look_attachment.remove(child) 43 | 44 | def update(self, input_object, delta_time): 45 | move_amount = self._units_per_second * delta_time 46 | rotate_amount = self._degrees_per_second * (math.pi / 180) * delta_time 47 | if input_object.is_key_pressed(self.KEY_MOVE_FORWARDS): 48 | self.translate(0, 0, -move_amount) 49 | if input_object.is_key_pressed(self.KEY_MOVE_BACKWARDS): 50 | self.translate(0, 0, move_amount) 51 | if input_object.is_key_pressed(self.KEY_MOVE_LEFT): 52 | self.translate(-move_amount, 0, 0) 53 | if input_object.is_key_pressed(self.KEY_MOVE_RIGHT): 54 | self.translate(move_amount, 0, 0) 55 | if input_object.is_key_pressed(self.KEY_MOVE_UP): 56 | self.translate(0, move_amount, 0) 57 | if input_object.is_key_pressed(self.KEY_MOVE_DOWN): 58 | self.translate(0, -move_amount, 0) 59 | if input_object.is_key_pressed(self.KEY_TURN_RIGHT): 60 | self.rotate_y(-rotate_amount) 61 | if input_object.is_key_pressed(self.KEY_TURN_LEFT): 62 | self.rotate_y(rotate_amount) 63 | if input_object.is_key_pressed(self.KEY_LOOK_UP): 64 | self._look_attachment.rotate_x(rotate_amount) 65 | if input_object.is_key_pressed(self.KEY_LOOK_DOWN): 66 | self._look_attachment.rotate_x(-rotate_amount) 67 | -------------------------------------------------------------------------------- /examples/2-11--key-control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import OpenGL.GL as GL 3 | 4 | from py3d.core.base import Base 5 | from py3d.core.utils import Utils 6 | from py3d.core.attribute import Attribute 7 | from py3d.core.uniform import Uniform 8 | 9 | 10 | class Example(Base): 11 | """ Enables the user to move a triangle using the arrow keys """ 12 | def initialize(self): 13 | print("Initializing program...") 14 | # Initialize program # 15 | vs_code = """ 16 | in vec3 position; 17 | uniform vec3 translation; 18 | void main() 19 | { 20 | vec3 pos = position + translation; 21 | gl_Position = vec4(pos.x, pos.y, pos.z, 1.0); 22 | } 23 | """ 24 | fs_code = """ 25 | uniform vec3 baseColor; 26 | out vec4 fragColor; 27 | void main() 28 | { 29 | fragColor = vec4(baseColor.r, baseColor.g, baseColor.b, 1.0); 30 | } 31 | """ 32 | self.program_ref = Utils.initialize_program(vs_code, fs_code) 33 | # render settings (optional) # 34 | # Specify color used when clearly 35 | GL.glClearColor(0.0, 0.0, 0.0, 1.0) 36 | # Set up vertex array object # 37 | vao_ref = GL.glGenVertexArrays(1) 38 | GL.glBindVertexArray(vao_ref) 39 | # Set up vertex attribute # 40 | position_data = [[ 0.0, 0.2, 0.0], 41 | [ 0.2, -0.2, 0.0], 42 | [-0.2, -0.2, 0.0]] 43 | self.vertex_count = len(position_data) 44 | position_attribute = Attribute('vec3', position_data) 45 | position_attribute.associate_variable(self.program_ref, 'position') 46 | # Set up uniforms # 47 | self.translation = Uniform('vec3', [-0.5, 0.0, 0.0]) 48 | self.translation.locate_variable(self.program_ref, 'translation') 49 | self.base_color = Uniform('vec3', [1.0, 0.0, 0.0]) 50 | self.base_color.locate_variable(self.program_ref, 'baseColor') 51 | # triangle speed, units per second 52 | self.speed = 0.5 53 | 54 | def update(self): 55 | """ Update data """ 56 | distance = self.speed * self.delta_time 57 | if self.input.is_key_pressed('left'): 58 | self.translation.data[0] -= distance 59 | if self.input.is_key_pressed('right'): 60 | self.translation.data[0] += distance 61 | if self.input.is_key_pressed('down'): 62 | self.translation.data[1] -= distance 63 | if self.input.is_key_pressed('up'): 64 | self.translation.data[1] += distance 65 | # Reset color buffer with specified color 66 | GL.glClear(GL.GL_COLOR_BUFFER_BIT) 67 | GL.glUseProgram(self.program_ref) 68 | self.translation.upload_data() 69 | self.base_color.upload_data() 70 | GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count) 71 | 72 | 73 | # Instantiate this class and run the program 74 | Example().run() 75 | -------------------------------------------------------------------------------- /py3d/material/sprite.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from py3d.material.material import Material 4 | 5 | 6 | class SpriteMaterial(Material): 7 | def __init__(self, texture, property_dict=None): 8 | vertex_shader_code = """ 9 | uniform mat4 projectionMatrix; 10 | uniform mat4 viewMatrix; 11 | uniform mat4 modelMatrix; 12 | uniform bool billboard; 13 | uniform float tileNumber; 14 | uniform vec2 tileCount; 15 | in vec3 vertexPosition; 16 | in vec2 vertexUV; 17 | out vec2 UV; 18 | 19 | void main() 20 | { 21 | mat4 mvMatrix = viewMatrix * modelMatrix; 22 | if (billboard) 23 | { 24 | mvMatrix[0][0] = 1; 25 | mvMatrix[0][1] = 0; 26 | mvMatrix[0][2] = 0; 27 | mvMatrix[1][0] = 0; 28 | mvMatrix[1][1] = 1; 29 | mvMatrix[1][2] = 0; 30 | mvMatrix[2][0] = 0; 31 | mvMatrix[2][1] = 0; 32 | mvMatrix[2][2] = 1; 33 | } 34 | 35 | gl_Position = projectionMatrix * mvMatrix * vec4(vertexPosition, 1.0); 36 | UV = vertexUV; 37 | if (tileNumber > -1.0) 38 | { 39 | vec2 tileSize = 1.0 / tileCount; 40 | float columnIndex = mod(tileNumber, tileCount[0]); 41 | float rowIndex = floor(tileNumber / tileCount[0]); 42 | vec2 tileOffset = vec2(columnIndex / tileCount[0], 1.0 - (rowIndex + 1.0) / tileCount[1]); 43 | UV = UV * tileSize + tileOffset; 44 | } 45 | } 46 | """ 47 | 48 | fragment_shader_code = """ 49 | uniform vec3 baseColor; 50 | uniform sampler2D textureSampler; 51 | in vec2 UV; 52 | out vec4 fragColor; 53 | void main() 54 | { 55 | vec4 color = vec4(baseColor, 1) * texture(textureSampler, UV); 56 | if (color.a < 0.1) 57 | discard; 58 | fragColor = color; 59 | } 60 | """ 61 | 62 | super().__init__(vertex_shader_code, fragment_shader_code) 63 | self.add_uniform("vec3", "baseColor", [1.0, 1.0, 1.0]) 64 | self.add_uniform("sampler2D", "textureSampler", [texture.texture_ref, 1]) 65 | self.add_uniform("bool", "billboard", False) 66 | self.add_uniform("float", "tileNumber", -1) 67 | self.add_uniform("vec2", "tileCount", [1, 1]) 68 | self.locate_uniforms() 69 | # Render both sides? 70 | self.setting_dict["doubleSide"] = True 71 | self.set_properties(property_dict) 72 | 73 | def update_render_settings(self): 74 | if self.setting_dict["doubleSide"]: 75 | GL.glDisable(GL.GL_CULL_FACE) 76 | else: 77 | GL.glEnable(GL.GL_CULL_FACE) -------------------------------------------------------------------------------- /py3d/core_ext/texture.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | import pygame 3 | 4 | 5 | class Texture: 6 | def __init__(self, file_name=None, property_dict={}): 7 | # Pygame object for storing pixel data; 8 | # can load from image or manipulate directly 9 | self._surface = None 10 | # reference of available texture from GPU 11 | self._texture_ref = GL.glGenTextures(1) 12 | # default property values 13 | self._property_dict = { 14 | "magFilter": GL.GL_LINEAR, 15 | "minFilter": GL.GL_LINEAR_MIPMAP_LINEAR, 16 | "wrap": GL.GL_REPEAT 17 | } 18 | # Overwrite default property values 19 | self.set_properties(property_dict) 20 | if file_name is not None: 21 | self.load_image(file_name) 22 | self.upload_data() 23 | 24 | @property 25 | def surface(self): 26 | return self._surface 27 | 28 | @surface.setter 29 | def surface(self, surface): 30 | self._surface = surface 31 | 32 | @property 33 | def texture_ref(self): 34 | return self._texture_ref 35 | 36 | def load_image(self, file_name): 37 | """ Load image from file """ 38 | self._surface = pygame.image.load(file_name) 39 | 40 | def set_properties(self, property_dict): 41 | """ Set property values """ 42 | if property_dict: 43 | for name, value in property_dict.items(): 44 | if name in self._property_dict.keys(): 45 | self._property_dict[name] = value 46 | else: # unknown property type 47 | raise Exception("Texture has no property with name: " + name) 48 | 49 | def upload_data(self): 50 | """ Upload pixel data to GPU """ 51 | # Store image dimensions 52 | width = self._surface.get_width() 53 | height = self._surface.get_height() 54 | # Convert image data to string buffer 55 | pixel_data = pygame.image.tostring(self._surface, "RGBA", True) 56 | # Specify texture used by the following functions 57 | GL.glBindTexture(GL.GL_TEXTURE_2D, self._texture_ref) 58 | # Send pixel data to texture buffer 59 | GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, width, height, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pixel_data) 60 | # Generate mipmap image from uploaded pixel data 61 | GL.glGenerateMipmap(GL.GL_TEXTURE_2D) 62 | # Specify technique for magnifying/minifying textures 63 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, self._property_dict["magFilter"]) 64 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, self._property_dict["minFilter"]) 65 | # Specify what happens to texture coordinates outside range [0, 1] 66 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, self._property_dict["wrap"]) 67 | GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, self._property_dict["wrap"]) 68 | # Set default border color to white; important for rendering shadows 69 | GL.glTexParameterfv(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_BORDER_COLOR, [1, 1, 1, 1]) -------------------------------------------------------------------------------- /examples/6-3-4--postprocessor-light-bloom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.rectangle import RectangleGeometry 11 | from py3d.geometry.sphere import SphereGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.postprocessor import Postprocessor 15 | from py3d.effects.bright_filter import BrightFilterEffect 16 | from py3d.effects.horizontal_blur import HorizontalBlurEffect 17 | from py3d.effects.vertical_blur import VerticalBlurEffect 18 | from py3d.effects.additive_blend import AdditiveBlendEffect 19 | 20 | 21 | class Example(Base): 22 | """ 23 | Demonstrate a light-bloom effect combining: 24 | - bright-filter effect 25 | - blur effect, and 26 | - light-bloom effect 27 | """ 28 | def initialize(self): 29 | print("Initializing program...") 30 | self.renderer = Renderer() 31 | self.scene = Scene() 32 | self.camera = Camera(aspect_ratio=800/600) 33 | self.rig = MovementRig() 34 | self.rig.add(self.camera) 35 | self.scene.add(self.rig) 36 | self.rig.set_position([0, 1, 4]) 37 | sky_geometry = SphereGeometry(radius=50) 38 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 39 | sky = Mesh(sky_geometry, sky_material) 40 | self.scene.add(sky) 41 | 42 | grass_geometry = RectangleGeometry(width=100, height=100) 43 | grass_material = TextureMaterial( 44 | texture=Texture(file_name="../py3d/images/grass.jpg"), 45 | property_dict={"repeatUV": [50, 50]} 46 | ) 47 | grass = Mesh(grass_geometry, grass_material) 48 | grass.rotate_x(-math.pi/2) 49 | self.scene.add(grass) 50 | 51 | sphere_geometry = SphereGeometry() 52 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 53 | self.sphere = Mesh(sphere_geometry, sphere_material) 54 | self.sphere.set_position([0, 1, 0]) 55 | self.scene.add(self.sphere) 56 | 57 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 58 | self.postprocessor.add_effect(BrightFilterEffect(1.5)) 59 | self.postprocessor.add_effect(HorizontalBlurEffect(texture_size=[800, 600], blur_radius=50)) 60 | self.postprocessor.add_effect(VerticalBlurEffect(texture_size=[800, 600], blur_radius=50)) 61 | main_scene = self.postprocessor.render_target_list[0].texture 62 | self.postprocessor.add_effect( 63 | AdditiveBlendEffect( 64 | blend_texture=main_scene, 65 | original_strength=2, 66 | blend_strength=1 67 | ) 68 | ) 69 | 70 | def update(self): 71 | self.sphere.rotate_y(0.01337) 72 | self.rig.update(self.input, self.delta_time) 73 | self.postprocessor.render() 74 | 75 | 76 | # Instantiate this class and run the program 77 | Example(screen_size=[800, 600]).run() 78 | -------------------------------------------------------------------------------- /examples/5-10--heads-up-display.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.geometry.box import BoxGeometry 11 | from py3d.geometry.rectangle import RectangleGeometry 12 | from py3d.material.texture import TextureMaterial 13 | from py3d.extras.movement_rig import MovementRig 14 | from py3d.extras.grid import GridHelper 15 | from py3d.extras.text_texture import TextTexture 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate a heads-up display (HUD): a transparent layer containing some images 21 | (for example, with a text), rendered after the main scene, and appearing on the top layer. 22 | Move the camera: WASDRF(move), QE(turn), TG(look). 23 | """ 24 | def initialize(self): 25 | print("Initializing program...") 26 | self.renderer = Renderer() 27 | self.scene = Scene() 28 | self.camera = Camera(aspect_ratio=800/600) 29 | self.rig = MovementRig() 30 | self.rig.add(self.camera) 31 | self.rig.set_position([0, 1.5, 5]) 32 | self.scene.add(self.rig) 33 | 34 | crate_geometry = BoxGeometry() 35 | crate_material = TextureMaterial(Texture("../py3d/images/crate.jpg")) 36 | crate = Mesh(crate_geometry, crate_material) 37 | crate.translate(0, 0.5, 0) 38 | self.scene.add(crate) 39 | 40 | grid = GridHelper(grid_color=[1, 1, 1], center_color=[1, 1, 0]) 41 | grid.rotate_x(-math.pi / 2) 42 | self.scene.add(grid) 43 | 44 | self.hud_scene = Scene() 45 | self.hud_camera = Camera() 46 | self.hud_camera.set_orthographic(0, 800, 0, 600, 1, -1) 47 | 48 | label_geometry1 = RectangleGeometry( 49 | width=400, height=200, 50 | position=[0, 600], 51 | alignment=[0, 1] 52 | ) 53 | label_material1 = TextureMaterial(Texture("../py3d/images/crate-simulator.png")) 54 | label1 = Mesh(label_geometry1, label_material1) 55 | self.hud_scene.add(label1) 56 | 57 | label_geometry2 = RectangleGeometry( 58 | width=200, height=200, 59 | position=[800, 0], 60 | alignment=[1, 0] 61 | ) 62 | message = TextTexture( 63 | text="Version 1.0", 64 | system_font_name="Ink Free", 65 | font_size=32, 66 | font_color=[127, 255, 127], 67 | image_width=200, 68 | image_height=200, 69 | transparent=True 70 | ) 71 | label_material2 = TextureMaterial(message) 72 | label2 = Mesh(label_geometry2, label_material2) 73 | self.hud_scene.add(label2) 74 | 75 | def update(self): 76 | self.rig.update(self.input, self.delta_time) 77 | self.renderer.render(self.scene, self.camera) 78 | self.renderer.render( 79 | scene=self.hud_scene, 80 | camera=self.hud_camera, 81 | clear_color=False 82 | ) 83 | 84 | 85 | # Instantiate this class and run the program 86 | Example(screen_size=[800, 600]).run() 87 | -------------------------------------------------------------------------------- /examples/5-11--render-targets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.render_target import RenderTarget 8 | from py3d.core_ext.renderer import Renderer 9 | from py3d.core_ext.scene import Scene 10 | from py3d.core_ext.texture import Texture 11 | from py3d.geometry.box import BoxGeometry 12 | from py3d.geometry.rectangle import RectangleGeometry 13 | from py3d.geometry.sphere import SphereGeometry 14 | from py3d.material.surface import SurfaceMaterial 15 | from py3d.material.texture import TextureMaterial 16 | from py3d.extras.movement_rig import MovementRig 17 | 18 | 19 | class Example(Base): 20 | """ 21 | Render a scene using two cameras onto two render targets. 22 | The first camera renders to the window. 23 | The second camera renders to a "television screen" (rectangle) making a texture. 24 | Move the first camera: WASDRF(move), QE(turn), TG(look). 25 | """ 26 | def initialize(self): 27 | print("Initializing program...") 28 | self.renderer = Renderer() 29 | self.scene = Scene() 30 | self.camera = Camera(aspect_ratio=800/600) 31 | self.rig = MovementRig() 32 | self.rig.add(self.camera) 33 | self.scene.add(self.rig) 34 | self.rig.set_position([0, 1, 4]) 35 | sky_geometry = SphereGeometry(radius=50) 36 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 37 | sky = Mesh(sky_geometry, sky_material) 38 | self.scene.add(sky) 39 | 40 | grass_geometry = RectangleGeometry(width=100, height=100) 41 | grass_material = TextureMaterial( 42 | texture=Texture(file_name="../py3d/images/grass.jpg"), 43 | property_dict={"repeatUV": [50, 50]} 44 | ) 45 | grass = Mesh(grass_geometry, grass_material) 46 | grass.rotate_x(-math.pi/2) 47 | self.scene.add(grass) 48 | 49 | sphere_geometry = SphereGeometry() 50 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 51 | self.sphere = Mesh(sphere_geometry, sphere_material) 52 | self.sphere.set_position([0, 1, 0]) 53 | self.scene.add(self.sphere) 54 | 55 | box_geometry = BoxGeometry(width=1.12, height=1.12, depth=0.2) 56 | box_material = SurfaceMaterial(property_dict={"baseColor": [0, 0, 0]}) 57 | box = Mesh(box_geometry, box_material) 58 | box.set_position([2, 1, 0]) 59 | self.scene.add(box) 60 | 61 | # Create the "television screen" on the box 62 | self.render_target = RenderTarget(resolution=[512, 512]) 63 | screen_geometry = RectangleGeometry(width=1.1, height=1.1) 64 | screen_material = TextureMaterial(self.render_target.texture) 65 | screen = Mesh(screen_geometry, screen_material) 66 | screen.set_position([2, 1, 0.11]) 67 | self.scene.add(screen) 68 | 69 | self.sky_camera = Camera(aspect_ratio=512/512) 70 | self.sky_camera.set_position([0, 10, 0]) 71 | self.sky_camera.look_at([0, 0, 0]) 72 | self.scene.add(self.sky_camera) 73 | 74 | def update(self): 75 | self.sphere.rotate_y(0.01337) 76 | self.rig.update(self.input, self.delta_time) 77 | self.renderer.render(self.scene, self.sky_camera, render_target=self.render_target) 78 | self.renderer.render(self.scene, self.camera) 79 | 80 | 81 | # Instantiate this class and run the program 82 | Example(screen_size=[800, 600]).run() 83 | -------------------------------------------------------------------------------- /examples/6-5--shadows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.camera import Camera 8 | from py3d.core_ext.mesh import Mesh 9 | from py3d.core_ext.texture import Texture 10 | from py3d.light.ambient import AmbientLight 11 | from py3d.light.directional import DirectionalLight 12 | from py3d.material.phong import PhongMaterial 13 | from py3d.material.texture import TextureMaterial 14 | from py3d.geometry.rectangle import RectangleGeometry 15 | from py3d.geometry.sphere import SphereGeometry 16 | from py3d.extras.movement_rig import MovementRig 17 | from py3d.extras.directional_light import DirectionalLightHelper 18 | 19 | 20 | class Example(Base): 21 | """ 22 | Render shadows using shadow pass by depth buffers for the directional light. 23 | """ 24 | def initialize(self): 25 | self.renderer = Renderer([0.2, 0.2, 0.2]) 26 | self.scene = Scene() 27 | self.camera = Camera(aspect_ratio=800/600) 28 | self.rig = MovementRig() 29 | self.rig.add(self.camera) 30 | self.rig.set_position([0, 2, 5]) 31 | 32 | ambient_light = AmbientLight(color=[0.2, 0.2, 0.2]) 33 | self.scene.add(ambient_light) 34 | self.directional_light = DirectionalLight(color=[0.5, 0.5, 0.5], direction=[-1, -1, 0]) 35 | # The directional light can take any position because it covers all the space. 36 | # The directional light helper is a child of the directional light. 37 | # So changing the global matrix of the parent leads to changing 38 | # the global matrix of its child. 39 | self.directional_light.set_position([2, 4, 0]) 40 | self.scene.add(self.directional_light) 41 | direct_helper = DirectionalLightHelper(self.directional_light) 42 | self.directional_light.add(direct_helper) 43 | 44 | sphere_geometry = SphereGeometry() 45 | phong_material = PhongMaterial( 46 | texture=Texture("../py3d/images/grid.jpg"), 47 | number_of_light_sources=2, 48 | use_shadow=True 49 | ) 50 | 51 | sphere1 = Mesh(sphere_geometry, phong_material) 52 | sphere1.set_position([-2, 1, 0]) 53 | self.scene.add(sphere1) 54 | 55 | sphere2 = Mesh(sphere_geometry, phong_material) 56 | sphere2.set_position([1, 2.2, -0.5]) 57 | self.scene.add(sphere2) 58 | 59 | self.renderer.enable_shadows(self.directional_light) 60 | 61 | """ 62 | # optional: render depth texture to mesh in scene 63 | depth_texture = self.renderer.shadow_object.render_target.texture 64 | shadow_display = Mesh(RectangleGeometry(), TextureMaterial(depth_texture)) 65 | shadow_display.set_position([-1, 3, 0]) 66 | self.scene.add(shadow_display) 67 | """ 68 | 69 | floor = Mesh(RectangleGeometry(width=20, height=20), phong_material) 70 | floor.rotate_x(-math.pi / 2) 71 | self.scene.add(floor) 72 | 73 | def update(self): 74 | #""" 75 | # view dynamic shadows -- need to increase shadow camera range 76 | self.directional_light.rotate_y(0.01337, False) 77 | #""" 78 | self.rig.update( self.input, self.delta_time) 79 | self.renderer.render(self.scene, self.camera) 80 | """ 81 | # render scene from shadow camera 82 | shadow_camera = self.renderer.shadow_object.camera 83 | self.renderer.render(self.scene, shadow_camera) 84 | """ 85 | 86 | 87 | # Instantiate this class and run the program 88 | Example(screen_size=[800, 600]).run() 89 | -------------------------------------------------------------------------------- /py3d/core/utils.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as GL 2 | 3 | from collections import namedtuple 4 | 5 | 6 | class Utils: 7 | """ 8 | Static methods to load and compile OpenGL shaders and link to create programs 9 | """ 10 | @staticmethod 11 | def get_system_info(): 12 | vendor = GL.glGetString(GL.GL_VENDOR).decode('utf-8') 13 | renderer = GL.glGetString(GL.GL_RENDERER).decode('utf-8') 14 | opengl = GL.glGetString(GL.GL_VERSION).decode('utf-8') 15 | glsl = GL.glGetString(GL.GL_SHADING_LANGUAGE_VERSION).decode('utf-8') 16 | Result = namedtuple('SystemInfo', ['vendor', 'renderer', 'opengl', 'glsl']) 17 | return Result(vendor, renderer, opengl, glsl) 18 | 19 | @staticmethod 20 | def initialize_shader(shader_code, shader_type): 21 | # Specify required OpenGL/GLSL version 22 | shader_code = '#version 330\n' + shader_code 23 | # Create empty shader object and return reference value 24 | shader_ref = GL.glCreateShader(shader_type) 25 | # Stores the source code in the shader 26 | GL.glShaderSource(shader_ref, shader_code) 27 | # Compiles source code previously stored in the shader object 28 | GL.glCompileShader(shader_ref) 29 | # Queries whether shader compile was successful 30 | compile_success = GL.glGetShaderiv(shader_ref, GL.GL_COMPILE_STATUS) 31 | if not compile_success: 32 | # Retrieve error message 33 | error_message = GL.glGetShaderInfoLog(shader_ref) 34 | # free memory used to store shader program 35 | GL.glDeleteShader(shader_ref) 36 | # Convert byte string to character string 37 | error_message = '\n' + error_message.decode('utf-8') 38 | # Raise exception: halt program and print error message 39 | raise Exception(error_message) 40 | # Compilation was successful; return shader reference value 41 | return shader_ref 42 | 43 | @staticmethod 44 | def initialize_program(vertex_shader_code, fragment_shader_code): 45 | vertex_shader_ref = Utils.initialize_shader(vertex_shader_code, GL.GL_VERTEX_SHADER) 46 | fragment_shader_ref = Utils.initialize_shader(fragment_shader_code, GL.GL_FRAGMENT_SHADER) 47 | # Create empty program object and store reference to it 48 | program_ref = GL.glCreateProgram() 49 | # Attach previously compiled shader programs 50 | GL.glAttachShader(program_ref, vertex_shader_ref) 51 | GL.glAttachShader(program_ref, fragment_shader_ref) 52 | # Link vertex shader to fragment shader 53 | GL.glLinkProgram(program_ref) 54 | # queries whether program link was successful 55 | link_success = GL.glGetProgramiv(program_ref, GL.GL_LINK_STATUS) 56 | if not link_success: 57 | # Retrieve error message 58 | error_message = GL.glGetProgramInfoLog(program_ref) 59 | # free memory used to store program 60 | GL.glDeleteProgram(program_ref) 61 | # Convert byte string to character string 62 | error_message = '\n' + error_message.decode('utf-8') 63 | # Raise exception: halt application and print error message 64 | raise Exception(error_message) 65 | # Linking was successful; return program reference value 66 | return program_ref 67 | 68 | @staticmethod 69 | def print_system_info(): 70 | info = Utils.get_system_info() 71 | result = ''.join(['Vendor: ', info.vendor, '\n', 72 | 'Renderer: ', info.renderer, '\n', 73 | 'OpenGL version supported: ', info.opengl, '\n', 74 | 'GLSL version supported: ', info.glsl]) 75 | print(result) 76 | -------------------------------------------------------------------------------- /examples/5-06-3--procedural-textures-lava.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.rectangle import RectangleGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ 13 | Generate a procedural fragment color by using uv-coordinates 14 | and also clouds, lava, marble, and wood grain textures 15 | """ 16 | 17 | def initialize(self): 18 | print("Initializing program...") 19 | self.renderer = Renderer() 20 | self.scene = Scene() 21 | self.camera = Camera(aspect_ratio=800 / 600) 22 | self.camera.set_position([0, 0, 1.5]) 23 | vertex_shader_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | in vec2 vertexUV; 29 | out vec2 UV; 30 | 31 | void main() 32 | { 33 | vec4 pos = vec4(vertexPosition, 1.0); 34 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos; 35 | UV = vertexUV; 36 | } 37 | """ 38 | fragment_shader_code = """ 39 | // Return a random value in [0, 1] 40 | float random(vec2 UV) 41 | { 42 | return fract(235711.0 * sin(14.337 * UV.x + 42.418 * UV.y)); 43 | } 44 | 45 | float boxRandom(vec2 UV, float scale) 46 | { 47 | vec2 iScaleUV = floor(scale * UV); 48 | return random(iScaleUV); 49 | } 50 | 51 | float smoothRandom(vec2 UV, float scale) 52 | { 53 | vec2 iScaleUV = floor(scale * UV); 54 | vec2 fScaleUV = fract(scale * UV); 55 | float a = random(iScaleUV); 56 | float b = random(round(iScaleUV + vec2(1, 0))); 57 | float c = random(round(iScaleUV + vec2(0, 1))); 58 | float d = random(round(iScaleUV + vec2(1, 1))); 59 | return mix(mix(a, b, fScaleUV.x), mix(c, d, fScaleUV.x), fScaleUV.y); 60 | } 61 | 62 | // Add smooth random values at different scales 63 | // weighted (amplitudes) so that sum is approximately 1.0 64 | float fractalLikeRandom(vec2 UV, float scale) 65 | { 66 | float value = 0.0; 67 | float amplitude = 0.5; 68 | for (int i = 0; i < 10; i++) 69 | { 70 | value += amplitude * smoothRandom(UV, scale); 71 | scale *= 2.0; 72 | amplitude *= 0.5; 73 | } 74 | return value; 75 | } 76 | 77 | in vec2 UV; 78 | out vec4 fragColor; 79 | void main() 80 | { 81 | // lava 82 | float r = fractalLikeRandom(UV, 40); 83 | vec4 color1 = vec4(1, 0.8, 0, 1); 84 | vec4 color2 = vec4(0.8, 0, 0, 1); 85 | fragColor = mix(color1, color2, r); 86 | } 87 | """ 88 | material = Material(vertex_shader_code, fragment_shader_code) 89 | material.locate_uniforms() 90 | 91 | geometry = RectangleGeometry() 92 | mesh = Mesh(geometry, material) 93 | self.scene.add(mesh) 94 | 95 | def update(self): 96 | self.renderer.render(self.scene, self.camera) 97 | 98 | 99 | # Instantiate this class and run the program 100 | Example(screen_size=[800, 600]).run() 101 | -------------------------------------------------------------------------------- /examples/5-06-2--procedural-textures-clouds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.rectangle import RectangleGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ 13 | Generate a procedural fragment color by using uv-coordinates 14 | and also clouds, lava, marble, and wood grain textures 15 | """ 16 | 17 | def initialize(self): 18 | print("Initializing program...") 19 | self.renderer = Renderer() 20 | self.scene = Scene() 21 | self.camera = Camera(aspect_ratio=800 / 600) 22 | self.camera.set_position([0, 0, 1.5]) 23 | vertex_shader_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | in vec2 vertexUV; 29 | out vec2 UV; 30 | 31 | void main() 32 | { 33 | vec4 pos = vec4(vertexPosition, 1.0); 34 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos; 35 | UV = vertexUV; 36 | } 37 | """ 38 | fragment_shader_code = """ 39 | // Return a random value in [0, 1] 40 | float random(vec2 UV) 41 | { 42 | return fract(235711.0 * sin(14.337 * UV.x + 42.418 * UV.y)); 43 | } 44 | 45 | float boxRandom(vec2 UV, float scale) 46 | { 47 | vec2 iScaleUV = floor(scale * UV); 48 | return random(iScaleUV); 49 | } 50 | 51 | float smoothRandom(vec2 UV, float scale) 52 | { 53 | vec2 iScaleUV = floor(scale * UV); 54 | vec2 fScaleUV = fract(scale * UV); 55 | float a = random(iScaleUV); 56 | float b = random(round(iScaleUV + vec2(1, 0))); 57 | float c = random(round(iScaleUV + vec2(0, 1))); 58 | float d = random(round(iScaleUV + vec2(1, 1))); 59 | return mix(mix(a, b, fScaleUV.x), mix(c, d, fScaleUV.x), fScaleUV.y); 60 | } 61 | 62 | // Add smooth random values at different scales 63 | // weighted (amplitudes) so that sum is approximately 1.0 64 | float fractalLikeRandom(vec2 UV, float scale) 65 | { 66 | float value = 0.0; 67 | float amplitude = 0.5; 68 | for (int i = 0; i < 10; i++) 69 | { 70 | value += amplitude * smoothRandom(UV, scale); 71 | scale *= 2.0; 72 | amplitude *= 0.5; 73 | } 74 | return value; 75 | } 76 | 77 | in vec2 UV; 78 | out vec4 fragColor; 79 | void main() 80 | { 81 | // clouds 82 | float r = fractalLikeRandom(UV, 5); 83 | vec4 color1 = vec4(0.5, 0.5, 1, 1); 84 | vec4 color2 = vec4(1, 1, 1, 1); 85 | fragColor = mix(color1, color2, r); 86 | } 87 | """ 88 | material = Material(vertex_shader_code, fragment_shader_code) 89 | material.locate_uniforms() 90 | 91 | geometry = RectangleGeometry() 92 | mesh = Mesh(geometry, material) 93 | self.scene.add(mesh) 94 | 95 | def update(self): 96 | self.renderer.render(self.scene, self.camera) 97 | 98 | 99 | # Instantiate this class and run the program 100 | Example(screen_size=[800, 600]).run() 101 | -------------------------------------------------------------------------------- /examples/5-06-4--procedural-textures-marble.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.rectangle import RectangleGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ 13 | Generate a procedural fragment color by using uv-coordinates 14 | and also clouds, lava, marble, and wood grain textures 15 | """ 16 | 17 | def initialize(self): 18 | print("Initializing program...") 19 | self.renderer = Renderer() 20 | self.scene = Scene() 21 | self.camera = Camera(aspect_ratio=800 / 600) 22 | self.camera.set_position([0, 0, 1.5]) 23 | vertex_shader_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | in vec2 vertexUV; 29 | out vec2 UV; 30 | 31 | void main() 32 | { 33 | vec4 pos = vec4(vertexPosition, 1.0); 34 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos; 35 | UV = vertexUV; 36 | } 37 | """ 38 | fragment_shader_code = """ 39 | // Return a random value in [0, 1] 40 | float random(vec2 UV) 41 | { 42 | return fract(235711.0 * sin(14.337 * UV.x + 42.418 * UV.y)); 43 | } 44 | 45 | float boxRandom(vec2 UV, float scale) 46 | { 47 | vec2 iScaleUV = floor(scale * UV); 48 | return random(iScaleUV); 49 | } 50 | 51 | float smoothRandom(vec2 UV, float scale) 52 | { 53 | vec2 iScaleUV = floor(scale * UV); 54 | vec2 fScaleUV = fract(scale * UV); 55 | float a = random(iScaleUV); 56 | float b = random(round(iScaleUV + vec2(1, 0))); 57 | float c = random(round(iScaleUV + vec2(0, 1))); 58 | float d = random(round(iScaleUV + vec2(1, 1))); 59 | return mix(mix(a, b, fScaleUV.x), mix(c, d, fScaleUV.x), fScaleUV.y); 60 | } 61 | 62 | // Add smooth random values at different scales 63 | // weighted (amplitudes) so that sum is approximately 1.0 64 | float fractalLikeRandom(vec2 UV, float scale) 65 | { 66 | float value = 0.0; 67 | float amplitude = 0.5; 68 | for (int i = 0; i < 10; i++) 69 | { 70 | value += amplitude * smoothRandom(UV, scale); 71 | scale *= 2.0; 72 | amplitude *= 0.5; 73 | } 74 | return value; 75 | } 76 | 77 | in vec2 UV; 78 | out vec4 fragColor; 79 | void main() 80 | { 81 | // marble 82 | float t = fractalLikeRandom(UV, 4); 83 | float r = abs(sin(20 * t)); 84 | vec4 color1 = vec4(0.0, 0.2, 0.0, 1.0); 85 | vec4 color2 = vec4(1.0, 1.0, 1.0, 1.0); 86 | fragColor = mix(color1, color2, r); 87 | } 88 | """ 89 | material = Material(vertex_shader_code, fragment_shader_code) 90 | material.locate_uniforms() 91 | 92 | geometry = RectangleGeometry() 93 | mesh = Mesh(geometry, material) 94 | self.scene.add(mesh) 95 | 96 | def update(self): 97 | self.renderer.render(self.scene, self.camera) 98 | 99 | 100 | # Instantiate this class and run the program 101 | Example(screen_size=[800, 600]).run() 102 | -------------------------------------------------------------------------------- /examples/5-06-1--procedural-textures-smoothed-random.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.rectangle import RectangleGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ 13 | Generate a procedural fragment color by using uv-coordinates 14 | and also clouds, lava, marble, and wood grain textures 15 | """ 16 | 17 | def initialize(self): 18 | print("Initializing program...") 19 | self.renderer = Renderer() 20 | self.scene = Scene() 21 | self.camera = Camera(aspect_ratio=800 / 600) 22 | self.camera.set_position([0, 0, 1.5]) 23 | vertex_shader_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | in vec2 vertexUV; 29 | out vec2 UV; 30 | 31 | void main() 32 | { 33 | vec4 pos = vec4(vertexPosition, 1.0); 34 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos; 35 | UV = vertexUV; 36 | } 37 | """ 38 | fragment_shader_code = """ 39 | // Return a random value in [0, 1] 40 | float random(vec2 UV) 41 | { 42 | return fract(235711.0 * sin(14.337 * UV.x + 42.418 * UV.y)); 43 | } 44 | 45 | float boxRandom(vec2 UV, float scale) 46 | { 47 | vec2 iScaleUV = floor(scale * UV); 48 | return random(iScaleUV); 49 | } 50 | 51 | float smoothRandom(vec2 UV, float scale) 52 | { 53 | vec2 iScaleUV = floor(scale * UV); 54 | vec2 fScaleUV = fract(scale * UV); 55 | float a = random(iScaleUV); 56 | float b = random(round(iScaleUV + vec2(1, 0))); 57 | float c = random(round(iScaleUV + vec2(0, 1))); 58 | float d = random(round(iScaleUV + vec2(1, 1))); 59 | return mix(mix(a, b, fScaleUV.x), mix(c, d, fScaleUV.x), fScaleUV.y); 60 | } 61 | 62 | // Add smooth random values at different scales 63 | // weighted (amplitudes) so that sum is approximately 1.0 64 | float fractalLikeRandom(vec2 UV, float scale) 65 | { 66 | float value = 0.0; 67 | float amplitude = 0.5; 68 | for (int i = 0; i < 10; i++) 69 | { 70 | value += amplitude * smoothRandom(UV, scale); 71 | scale *= 2.0; 72 | amplitude *= 0.5; 73 | } 74 | return value; 75 | } 76 | 77 | in vec2 UV; 78 | out vec4 fragColor; 79 | void main() 80 | { 81 | // smoothed random color 82 | // float r = random(UV); 83 | // float r = boxRandom(UV, 10); 84 | // float r = smoothRandom(UV, 10); 85 | float r = fractalLikeRandom(UV, 10); 86 | fragColor = vec4(r, r, r, 1); 87 | } 88 | """ 89 | material = Material(vertex_shader_code, fragment_shader_code) 90 | material.locate_uniforms() 91 | 92 | geometry = RectangleGeometry() 93 | mesh = Mesh(geometry, material) 94 | self.scene.add(mesh) 95 | 96 | def update(self): 97 | self.renderer.render(self.scene, self.camera) 98 | 99 | 100 | # Instantiate this class and run the program 101 | Example(screen_size=[800, 600]).run() 102 | -------------------------------------------------------------------------------- /examples/6-4--postprocessing-glow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.core_ext.render_target import RenderTarget 11 | from py3d.geometry.rectangle import RectangleGeometry 12 | from py3d.geometry.sphere import SphereGeometry 13 | from py3d.material.texture import TextureMaterial 14 | from py3d.material.surface import SurfaceMaterial 15 | from py3d.extras.movement_rig import MovementRig 16 | from py3d.extras.postprocessor import Postprocessor 17 | from py3d.effects.horizontal_blur import HorizontalBlurEffect 18 | from py3d.effects.vertical_blur import VerticalBlurEffect 19 | from py3d.effects.additive_blend import AdditiveBlendEffect 20 | 21 | 22 | class Example(Base): 23 | """ 24 | Demonstrate a glow effect using two scenes. 25 | The first one renders the glow effect (blur of a red-material sphere). 26 | The second (main) one renders blending the first one and the original scene. 27 | """ 28 | def initialize(self): 29 | print("Initializing program...") 30 | self.renderer = Renderer(clear_color=[0, 0, 0]) 31 | self.scene = Scene() 32 | self.camera = Camera(aspect_ratio=800/600) 33 | self.rig = MovementRig() 34 | self.rig.add(self.camera) 35 | self.scene.add(self.rig) 36 | self.rig.set_position([0, 1, 4]) 37 | sky_geometry = SphereGeometry(radius=50) 38 | sky_material = TextureMaterial(texture=Texture(file_name="../py3d/images/sky.jpg")) 39 | sky = Mesh(sky_geometry, sky_material) 40 | self.scene.add(sky) 41 | 42 | grass_geometry = RectangleGeometry(width=100, height=100) 43 | grass_material = TextureMaterial( 44 | texture=Texture(file_name="../py3d/images/grass.jpg"), 45 | property_dict={"repeatUV": [50, 50]} 46 | ) 47 | grass = Mesh(grass_geometry, grass_material) 48 | grass.rotate_x(-math.pi/2) 49 | self.scene.add(grass) 50 | 51 | sphere_geometry = SphereGeometry() 52 | sphere_material = TextureMaterial(Texture("../py3d/images/grid.jpg")) 53 | self.sphere = Mesh(sphere_geometry, sphere_material) 54 | self.sphere.set_position([0, 1, 0]) 55 | self.scene.add(self.sphere) 56 | 57 | self.postprocessor = Postprocessor(self.renderer, self.scene, self.camera) 58 | 59 | # glow scene 60 | self.glow_scene = Scene() 61 | red_material = SurfaceMaterial(property_dict={"baseColor": [1, 0, 0]}) 62 | glow_sphere = Mesh(sphere_geometry, red_material) 63 | glow_sphere.local_matrix = self.sphere.local_matrix 64 | self.glow_scene.add(glow_sphere) 65 | 66 | # glow postprocessing 67 | glow_target = RenderTarget(resolution=[800, 600]) 68 | self.glow_pass = Postprocessor(self.renderer, self.glow_scene, self.camera, glow_target) 69 | self.glow_pass.add_effect(HorizontalBlurEffect(texture_size=[800, 600], blur_radius=50)) 70 | self.glow_pass.add_effect(VerticalBlurEffect(texture_size=[800, 600], blur_radius=50)) 71 | 72 | # combining results of glow effect with main scene 73 | self.combo_pass = Postprocessor(self.renderer, self.scene, self.camera) 74 | self.combo_pass.add_effect( 75 | AdditiveBlendEffect( 76 | blend_texture=glow_target.texture, 77 | original_strength=1, 78 | blend_strength=3 79 | ) 80 | ) 81 | 82 | def update(self): 83 | self.sphere.rotate_y(0.01337) 84 | self.rig.update(self.input, self.delta_time) 85 | self.glow_pass.render() 86 | self.combo_pass.render() 87 | 88 | 89 | # Instantiate this class and run the program 90 | Example(screen_size=[800, 600]).run() 91 | -------------------------------------------------------------------------------- /examples/5-06-5--procedural-textures-wood-grain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.geometry.rectangle import RectangleGeometry 8 | from py3d.material.material import Material 9 | 10 | 11 | class Example(Base): 12 | """ 13 | Generate a procedural fragment color by using uv-coordinates 14 | and also clouds, lava, marble, and wood grain textures 15 | """ 16 | 17 | def initialize(self): 18 | print("Initializing program...") 19 | self.renderer = Renderer() 20 | self.scene = Scene() 21 | self.camera = Camera(aspect_ratio=800 / 600) 22 | self.camera.set_position([0, 0, 1.5]) 23 | vertex_shader_code = """ 24 | uniform mat4 projectionMatrix; 25 | uniform mat4 viewMatrix; 26 | uniform mat4 modelMatrix; 27 | in vec3 vertexPosition; 28 | in vec2 vertexUV; 29 | out vec2 UV; 30 | 31 | void main() 32 | { 33 | vec4 pos = vec4(vertexPosition, 1.0); 34 | gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos; 35 | UV = vertexUV; 36 | } 37 | """ 38 | fragment_shader_code = """ 39 | // Return a random value in [0, 1] 40 | float random(vec2 UV) 41 | { 42 | return fract(235711.0 * sin(14.337 * UV.x + 42.418 * UV.y)); 43 | } 44 | 45 | float boxRandom(vec2 UV, float scale) 46 | { 47 | vec2 iScaleUV = floor(scale * UV); 48 | return random(iScaleUV); 49 | } 50 | 51 | float smoothRandom(vec2 UV, float scale) 52 | { 53 | vec2 iScaleUV = floor(scale * UV); 54 | vec2 fScaleUV = fract(scale * UV); 55 | float a = random(iScaleUV); 56 | float b = random(round(iScaleUV + vec2(1, 0))); 57 | float c = random(round(iScaleUV + vec2(0, 1))); 58 | float d = random(round(iScaleUV + vec2(1, 1))); 59 | return mix(mix(a, b, fScaleUV.x), mix(c, d, fScaleUV.x), fScaleUV.y); 60 | } 61 | 62 | // Add smooth random values at different scales 63 | // weighted (amplitudes) so that sum is approximately 1.0 64 | float fractalLikeRandom(vec2 UV, float scale) 65 | { 66 | float value = 0.0; 67 | float amplitude = 0.5; 68 | for (int i = 0; i < 10; i++) 69 | { 70 | value += amplitude * smoothRandom(UV, scale); 71 | scale *= 2.0; 72 | amplitude *= 0.5; 73 | } 74 | return value; 75 | } 76 | 77 | in vec2 UV; 78 | out vec4 fragColor; 79 | void main() 80 | { 81 | // wood grain 82 | float t = 80 * UV.y + 20 * fractalLikeRandom(UV, 2); 83 | float r = clamp(2 * abs(sin(t)), 0, 1); 84 | vec4 color1 = vec4(0.3, 0.2, 0.0, 1.0); 85 | vec4 color2 = vec4(0.6, 0.4, 0.2, 1.0); 86 | fragColor = mix(color1, color2, r); 87 | } 88 | """ 89 | material = Material(vertex_shader_code, fragment_shader_code) 90 | material.locate_uniforms() 91 | 92 | geometry = RectangleGeometry() 93 | mesh = Mesh(geometry, material) 94 | self.scene.add(mesh) 95 | 96 | def update(self): 97 | self.renderer.render(self.scene, self.camera) 98 | 99 | 100 | # Instantiate this class and run the program 101 | Example(screen_size=[800, 600]).run() 102 | -------------------------------------------------------------------------------- /py3d/core/matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | 4 | 5 | class Matrix: 6 | """ 7 | Contains static methods to generate matrices (with the numpy library) corresponding 8 | to identity, translation, rotation (around each axis), scaling, and projection. 9 | """ 10 | @staticmethod 11 | def make_identity(): 12 | return np.array( 13 | [[1, 0, 0, 0], 14 | [0, 1, 0, 0], 15 | [0, 0, 1, 0], 16 | [0, 0, 0, 1]] 17 | ).astype(float) 18 | 19 | @staticmethod 20 | def make_translation(x, y, z): 21 | return np.array( 22 | [[1, 0, 0, x], 23 | [0, 1, 0, y], 24 | [0, 0, 1, z], 25 | [0, 0, 0, 1]] 26 | ).astype(float) 27 | 28 | @staticmethod 29 | def make_rotation_x(angle): 30 | c = math.cos(angle) 31 | s = math.sin(angle) 32 | return np.array( 33 | [[1, 0, 0, 0], 34 | [0, c, -s, 0], 35 | [0, s, c, 0], 36 | [0, 0, 0, 1]] 37 | ).astype(float) 38 | 39 | @staticmethod 40 | def make_rotation_y(angle): 41 | c = math.cos(angle) 42 | s = math.sin(angle) 43 | return np.array( 44 | [[c, 0, s, 0], 45 | [0, 1, 0, 0], 46 | [-s, 0, c, 0], 47 | [0, 0, 0, 1]] 48 | ).astype(float) 49 | 50 | @staticmethod 51 | def make_rotation_z(angle): 52 | c = math.cos(angle) 53 | s = math.sin(angle) 54 | return np.array( 55 | [[c, -s, 0, 0], 56 | [s, c, 0, 0], 57 | [0, 0, 1, 0], 58 | [0, 0, 0, 1]] 59 | ).astype(float) 60 | 61 | @staticmethod 62 | def make_scale(s): 63 | return np.array( 64 | [[s, 0, 0, 0], 65 | [0, s, 0, 0], 66 | [0, 0, s, 0], 67 | [0, 0, 0, 1]] 68 | ).astype(float) 69 | 70 | @staticmethod 71 | def make_perspective(angle_of_view=60, aspect_ratio=1, near=0.1, far=1000): 72 | a = angle_of_view * math.pi / 180.0 73 | d = 1.0 / math.tan(a / 2) 74 | b = (far + near) / (near - far) 75 | c = 2 * far * near / (near - far) 76 | return np.array( 77 | [[d / aspect_ratio, 0, 0, 0], 78 | [0, d, 0, 0], 79 | [0, 0, b, c], 80 | [0, 0, -1, 0]] 81 | ).astype(float) 82 | 83 | @staticmethod 84 | def make_orthographic(left=-1, right=1, bottom=-1, top=1, near=-1, far=1): 85 | return np.array( 86 | [[2 / (right - left), 0, 0, -(right + left) / (right - left)], 87 | [0, 2 / (top - bottom), 0, -(top + bottom) / (top - bottom)], 88 | [0, 0, -2 / (far - near), -(far + near) / (far - near)], 89 | [0, 0, 0, 1]] 90 | ).astype(float) 91 | 92 | @staticmethod 93 | def make_look_at(position, target): 94 | world_up = [0, 1, 0] 95 | forward = np.subtract(target, position) 96 | right = np.cross(forward, world_up) 97 | # If forward and world_up vectors are parallel, 98 | # the right vector is zero. 99 | # Fix this by perturbing the world_up vector a bit 100 | if np.linalg.norm(right) < 1e-6: 101 | offset = np.array([0, 0, -1e-3]) 102 | right = np.cross(forward, world_up + offset) 103 | up = np.cross(right, forward) 104 | # All vectors should have length 1 105 | forward = np.divide(forward, np.linalg.norm(forward)) 106 | right = np.divide(right, np.linalg.norm(right)) 107 | up = np.divide(up, np.linalg.norm(up)) 108 | return np.array( 109 | [[right[0], up[0], -forward[0], position[0]], 110 | [right[1], up[1], -forward[1], position[1]], 111 | [right[2], up[2], -forward[2], position[2]], 112 | [0, 0, 0, 1]] 113 | ).astype(float) 114 | -------------------------------------------------------------------------------- /examples/6-1-2--dynamical-lighting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | 4 | from py3d.core.base import Base 5 | from py3d.core_ext.camera import Camera 6 | from py3d.core_ext.mesh import Mesh 7 | from py3d.core_ext.renderer import Renderer 8 | from py3d.core_ext.scene import Scene 9 | from py3d.core_ext.texture import Texture 10 | from py3d.extras.directional_light import DirectionalLightHelper 11 | from py3d.extras.movement_rig import MovementRig 12 | from py3d.extras.point_light import PointLightHelper 13 | from py3d.geometry.sphere import SphereGeometry 14 | from py3d.light.ambient import AmbientLight 15 | from py3d.light.directional import DirectionalLight 16 | from py3d.light.point import PointLight 17 | from py3d.material.flat import FlatMaterial 18 | from py3d.material.lambert import LambertMaterial 19 | from py3d.material.phong import PhongMaterial 20 | 21 | 22 | class Example(Base): 23 | """ 24 | Demonstrate dynamical lighting with: 25 | - the flat shading model; 26 | - the Lambert illumination model and Phong shading model; 27 | - the Phong illumination model and Phong shading model; 28 | and render light helpers that show a light position and 29 | a light direction for a point light and a directional light, 30 | respectively. 31 | 32 | Move a camera: WASDRF(move), QE(turn), TG(look). 33 | """ 34 | def initialize(self): 35 | print("Initializing program...") 36 | self.renderer = Renderer() 37 | self.scene = Scene() 38 | self.camera = Camera(aspect_ratio=800/600) 39 | self.rig = MovementRig() 40 | self.rig.add(self.camera) 41 | self.rig.set_position([0, 0, 6]) 42 | self.scene.add(self.rig) 43 | 44 | # three light sources 45 | ambient_light = AmbientLight(color=[0.1, 0.1, 0.1]) 46 | self.scene.add(ambient_light) 47 | self.directional_light = DirectionalLight(color=[0.8, 0.8, 0.8], direction=[-1, -1, 0]) 48 | self.scene.add(self.directional_light) 49 | self.point_light = PointLight(color=[0.9, 0, 0], position=[1, 1, 0.8]) 50 | self.scene.add(self.point_light) 51 | 52 | # lighted materials with a color 53 | flat_material = FlatMaterial( 54 | property_dict={"baseColor": [0.2, 0.5, 0.5]}, 55 | number_of_light_sources=3 56 | ) 57 | lambert_material = LambertMaterial( 58 | property_dict={"baseColor": [0.2, 0.5, 0.5]}, 59 | number_of_light_sources=3 60 | ) 61 | phong_material = PhongMaterial( 62 | property_dict={"baseColor": [0.2, 0.5, 0.5]}, 63 | number_of_light_sources=3 64 | ) 65 | 66 | # lighted spheres with a color 67 | sphere_geometry = SphereGeometry() 68 | sphere_left = Mesh(sphere_geometry, flat_material) 69 | sphere_left.set_position([-2.5, 0, 0]) 70 | self.scene.add(sphere_left) 71 | sphere_center = Mesh(sphere_geometry, lambert_material) 72 | sphere_center.set_position([0, 0, 0]) 73 | self.scene.add(sphere_center) 74 | sphere_right = Mesh(sphere_geometry, phong_material) 75 | sphere_right.set_position([2.5, 0, 0]) 76 | self.scene.add(sphere_right) 77 | 78 | # helpers 79 | directional_light_helper = DirectionalLightHelper(self.directional_light) 80 | # The directional light can take any position because it covers all the space. 81 | # The directional light helper is a child of the directional light. 82 | # So changing the global matrix of the parent leads to changing 83 | # the global matrix of its child. 84 | self.directional_light.set_position([0, 2, 0]) 85 | self.directional_light.add(directional_light_helper) 86 | point_light_helper = PointLightHelper(self.point_light) 87 | self.point_light.add(point_light_helper) 88 | 89 | def update(self): 90 | self.rig.update(self.input, self.delta_time) 91 | self.directional_light.set_direction([-1, math.sin(0.5 * self.time), 0]) 92 | self.point_light.set_position([1, math.sin(self.time), 1]) 93 | self.renderer.render(self.scene, self.camera) 94 | 95 | 96 | # Instantiate this class and run the program 97 | Example(screen_size=[800, 600]).run() 98 | -------------------------------------------------------------------------------- /py3d/geometry/geometry.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from py3d.core.attribute import Attribute 3 | 4 | 5 | class Geometry: 6 | """ Stores attribute data and the total number of vertices """ 7 | def __init__(self): 8 | # Store Attribute objects, indexed by name of associated variable in shader. 9 | # Shader variable associations set up later and stored in vertex array object in Mesh. 10 | self._attribute_dict = {} 11 | # number of vertices 12 | self._vertex_count = None 13 | 14 | @property 15 | def attribute_dict(self): 16 | return self._attribute_dict 17 | 18 | @property 19 | def vertex_count(self): 20 | return self._vertex_count 21 | 22 | def add_attribute(self, data_type, variable_name, data): 23 | attribute = Attribute(data_type, data) 24 | self._attribute_dict[variable_name] = attribute 25 | # Update the vertex count 26 | if variable_name == "vertexPosition": 27 | # Number of vertices may be calculated from 28 | # the length of any Attribute object's array of data 29 | self._vertex_count = len(data) 30 | 31 | def upload_data(self, variable_names=None): 32 | if not variable_names: 33 | variable_names = self._attribute_dict.keys() 34 | for variable_name in variable_names: 35 | self._attribute_dict[variable_name].upload_data() 36 | # Update the vertex count 37 | if variable_name == "vertexPosition": 38 | # Number of vertices may be calculated from 39 | # the length of any Attribute object's array of data 40 | self._vertex_count = len(self._attribute_dict[variable_name].data) 41 | 42 | def apply_matrix(self, matrix): 43 | """ Transform the data in an attribute using a matrix """ 44 | old_position_data = self._attribute_dict["vertexPosition"].data 45 | new_position_data = [] 46 | for old_pos in old_position_data: 47 | # Avoid changing list references 48 | new_pos = old_pos.copy() 49 | # Add the homogeneous fourth coordinate 50 | new_pos.append(1) 51 | # Multiply by matrix. 52 | # No need to transform new_pos to np.array. 53 | new_pos = matrix @ new_pos 54 | # Remove the homogeneous coordinate 55 | new_pos = list(new_pos[0:3]) 56 | # Add to the new data list 57 | new_position_data.append(new_pos) 58 | self._attribute_dict["vertexPosition"].data = new_position_data 59 | # New data must be uploaded 60 | self._attribute_dict["vertexPosition"].upload_data() 61 | self._vertex_count = len(new_position_data) 62 | 63 | # Extract the rotation submatrix 64 | rotation_matrix = np.array( 65 | [matrix[0][0:3], 66 | matrix[1][0:3], 67 | matrix[2][0:3]] 68 | ).astype(float) 69 | 70 | old_vertex_normal_data = self._attribute_dict["vertexNormal"].data 71 | new_vertex_normal_data = [] 72 | for old_normal in old_vertex_normal_data: 73 | # Avoid changing list references 74 | new_normal = old_normal.copy() 75 | new_normal = rotation_matrix @ new_normal 76 | new_vertex_normal_data.append(new_normal) 77 | self._attribute_dict["vertexNormal"].data = new_vertex_normal_data 78 | # New data must be uploaded 79 | self._attribute_dict["vertexNormal"].upload_data() 80 | 81 | old_face_normal_data = self._attribute_dict["faceNormal"].data 82 | new_face_normal_data = [] 83 | for old_normal in old_face_normal_data: 84 | # Avoid changing list references 85 | new_normal = old_normal.copy() 86 | new_normal = rotation_matrix @ new_normal 87 | new_face_normal_data.append(new_normal) 88 | self._attribute_dict["faceNormal"].data = new_face_normal_data 89 | # New data must be uploaded 90 | self._attribute_dict["faceNormal"].upload_data() 91 | 92 | def merge(self, other_geometry): 93 | """ 94 | Merge data from attributes of other geometry into this object. 95 | Requires both geometries to have attributes with same names. 96 | """ 97 | for variable_name, attribute_instance in self._attribute_dict.items(): 98 | attribute_instance.data.extend(other_geometry.attribute_dict[variable_name].data) 99 | # New data must be uploaded 100 | attribute_instance.upload_data() 101 | 102 | -------------------------------------------------------------------------------- /examples/6-1-1--lighted-spheres.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from py3d.core.base import Base 3 | from py3d.core_ext.camera import Camera 4 | from py3d.core_ext.mesh import Mesh 5 | from py3d.core_ext.renderer import Renderer 6 | from py3d.core_ext.scene import Scene 7 | from py3d.core_ext.texture import Texture 8 | from py3d.extras.movement_rig import MovementRig 9 | from py3d.geometry.sphere import SphereGeometry 10 | from py3d.light.ambient import AmbientLight 11 | from py3d.light.directional import DirectionalLight 12 | from py3d.light.point import PointLight 13 | from py3d.material.flat import FlatMaterial 14 | from py3d.material.lambert import LambertMaterial 15 | from py3d.material.phong import PhongMaterial 16 | 17 | 18 | class Example(Base): 19 | """ 20 | Demonstrate: 21 | - the flat shading model; 22 | - the Lambert illumination model and Phong shading model; 23 | - the Phong illumination model and Phong shading model. 24 | The Lambert illumination model uses a combination of ambient and diffuse lighting. 25 | The Phong illumination model uses ambient, diffuse, and specular lighting. 26 | In the flat shading model, the light calculations are performed in the vertex shader. 27 | In the Phong shading model, the light calculations are performed in the fragment shader. 28 | 29 | Move a camera: WASDRF(move), QE(turn), TG(look). 30 | """ 31 | def initialize(self): 32 | print("Initializing program...") 33 | self.renderer = Renderer() 34 | self.scene = Scene() 35 | self.camera = Camera(aspect_ratio=800/600) 36 | self.rig = MovementRig() 37 | self.rig.add(self.camera) 38 | self.rig.set_position([0, 0, 6]) 39 | self.scene.add(self.rig) 40 | 41 | # four light sources 42 | ambient_light = AmbientLight(color=[0.1, 0.1, 0.1]) 43 | self.scene.add(ambient_light) 44 | directional_light = DirectionalLight(color=[0.8, 0.8, 0.8], direction=[-1, -1, -2]) 45 | self.scene.add(directional_light) 46 | point_light1 = PointLight(color=[0.9, 0, 0], position=[4, 0, 0]) 47 | self.scene.add(point_light1) 48 | point_light2 = PointLight(color=[0, 0.9, 0], position=[-4, 0, 0]) 49 | self.scene.add(point_light2) 50 | 51 | # lighted materials with a color 52 | flat_material = FlatMaterial( 53 | property_dict={"baseColor": [0.2, 0.5, 0.5]}, 54 | number_of_light_sources=4 55 | ) 56 | lambert_material = LambertMaterial( 57 | property_dict={"baseColor": [0.2, 0.5, 0.5]}, 58 | number_of_light_sources=4 59 | ) 60 | phong_material = PhongMaterial( 61 | property_dict={"baseColor": [0.2, 0.5, 0.5]}, 62 | number_of_light_sources=4 63 | ) 64 | 65 | # lighted spheres with a color 66 | sphere_geometry = SphereGeometry() 67 | sphere_left_top = Mesh(sphere_geometry, flat_material) 68 | sphere_left_top.set_position([-2.5, 1.5, 0]) 69 | self.scene.add(sphere_left_top) 70 | sphere_center_top = Mesh(sphere_geometry, lambert_material) 71 | sphere_center_top.set_position([0, 1.5, 0]) 72 | self.scene.add(sphere_center_top) 73 | sphere_right_top = Mesh(sphere_geometry, phong_material) 74 | sphere_right_top.set_position([2.5, 1.5, 0]) 75 | self.scene.add(sphere_right_top) 76 | 77 | # lighted materials with a texture 78 | textured_flat_material = FlatMaterial( 79 | texture=Texture("../py3d/images/grid.jpg"), 80 | number_of_light_sources=4 81 | ) 82 | textured_lambert_material = LambertMaterial( 83 | texture=Texture("../py3d/images/grid.jpg"), 84 | number_of_light_sources=4 85 | ) 86 | textured_phong_material = PhongMaterial( 87 | texture=Texture("../py3d/images/grid.jpg"), 88 | number_of_light_sources=4 89 | ) 90 | 91 | # lighted spheres with a texture 92 | sphere_left_bottom = Mesh(sphere_geometry, textured_flat_material) 93 | sphere_left_bottom.set_position([-2.5, -1.5, 0]) 94 | self.scene.add(sphere_left_bottom) 95 | sphere_center_bottom = Mesh(sphere_geometry, textured_lambert_material) 96 | sphere_center_bottom.set_position([0, -1.5, 0]) 97 | self.scene.add(sphere_center_bottom) 98 | sphere_right_bottom = Mesh(sphere_geometry, textured_phong_material) 99 | sphere_right_bottom.set_position([2.5, -1.5, 0]) 100 | self.scene.add(sphere_right_bottom) 101 | 102 | def update(self): 103 | self.rig.update(self.input, self.delta_time) 104 | self.renderer.render(self.scene, self.camera) 105 | 106 | 107 | # Instantiate this class and run the program 108 | Example(screen_size=[800, 600]).run() 109 | --------------------------------------------------------------------------------