diff --git a/.vscode/launch.json b/.vscode/launch.json index c60156f..f81efaf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,15 +10,7 @@ "request": "launch", "program": "main.py", "console": "integratedTerminal", - "cwd": "${workspaceFolder}" - }, - { - "name": "Python: test.py", - "type": "python", - "request": "launch", - "program": "test.py", - "console": "integratedTerminal", - "cwd": "${workspaceFolder}" + "cwd": "${workspaceFolder}", } ] } \ No newline at end of file diff --git a/README.md b/README.md index 5384a69..0ffbd16 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,17 @@ Make sure to have python3 running and the following libs are installed: `pip install pillow` -## What is special about this fork? +Blender generate .obj-file steps: -For the general idea please visit [https://github.com/ssloy/tinyrenderer](https://github.com/ssloy/tinyrenderer) +1. object mode -> Object -> Convert to -> Mesh from Curve/Meta/Surf/Text +1. remove all keyframes +1. delete rigs, cam, particles ... +1. move objects -This fork is intended to be a starting point of teaching programming in python3 to everyone as I consider the content to be tough but motivational for beginners. -With this in mind I tried to follow ssloys tutorial closely and chose some guidelines: +1. add Decimate modifier to one object +1. select all in viewpane +1. ctrl + l -> Modifier to add last selected modifier in 1. sidepane to all objects +1. set origin to 3d cursor -1. Simplicity over perfection -1. Readability over performance - -To accomplish these goals I included a special linear algebra module `geom.py` instead of using `numpy`. -Numpy is undoubtly more universal and elaborated but harder to read for beginners. -The `geom.py` module contains some basic vector and matrix classes and supports indexing via '.x', '.y' and '.z' notation based on namedtuples. - -After finishing the fork I am going to be working on course material and structured lessons to roll out this exciting course of 3D animation and programming sometime in the future. To motivate students even more I plan providing some short Blender tutorials on how to prepare custom models for this renderer. - -## Here are some examples generated by the renderer - -### Rendered image with shadows, ambient occlusion and specular lighting (current optimum) -Rendered image with shadows and specular lighting - -### Rendered wire mesh -Rendered wire mesh - -### Rendered randomly colored filled mesh triangles -Randomly colored filled mesh triangles +1. select all +1. export obj (triangulate, no obj groups, selected only, write normals) \ No newline at end of file diff --git a/docs/images/ao_map_shade.png b/docs/images/ao_map_shade.png deleted file mode 100644 index caa7149..0000000 Binary files a/docs/images/ao_map_shade.png and /dev/null differ diff --git a/docs/images/diffuse_gouraud_interpolation_fail_wrong_matmul.png b/docs/images/diffuse_gouraud_interpolation_fail_wrong_matmul.png deleted file mode 100644 index b1ea056..0000000 Binary files a/docs/images/diffuse_gouraud_interpolation_fail_wrong_matmul.png and /dev/null differ diff --git a/docs/images/diffuse_gouraud_shade.png b/docs/images/diffuse_gouraud_shade.png deleted file mode 100644 index 3192d45..0000000 Binary files a/docs/images/diffuse_gouraud_shade.png and /dev/null differ diff --git a/docs/images/e04_autumn_mesh.png b/docs/images/e04_autumn_mesh.png deleted file mode 100644 index dd8bf46..0000000 Binary files a/docs/images/e04_autumn_mesh.png and /dev/null differ diff --git a/docs/images/e06_autumn_filled.png b/docs/images/e06_autumn_filled.png deleted file mode 100644 index 172c8ab..0000000 Binary files a/docs/images/e06_autumn_filled.png and /dev/null differ diff --git a/docs/images/e08.1_autumn_flat_shaded.png b/docs/images/e08.1_autumn_flat_shaded.png deleted file mode 100644 index acddf38..0000000 Binary files a/docs/images/e08.1_autumn_flat_shaded.png and /dev/null differ diff --git a/docs/images/e08.2_autumn_zbuffered.png b/docs/images/e08.2_autumn_zbuffered.png deleted file mode 100644 index f24e352..0000000 Binary files a/docs/images/e08.2_autumn_zbuffered.png and /dev/null differ diff --git a/docs/images/e09.1_autumn_perspective.png b/docs/images/e09.1_autumn_perspective.png deleted file mode 100644 index e3029c8..0000000 Binary files a/docs/images/e09.1_autumn_perspective.png and /dev/null differ diff --git a/docs/images/e09.2_autumn_texture.png b/docs/images/e09.2_autumn_texture.png deleted file mode 100644 index cb5ad73..0000000 Binary files a/docs/images/e09.2_autumn_texture.png and /dev/null differ diff --git a/docs/images/estimated_ao_shade.png b/docs/images/estimated_ao_shade.png deleted file mode 100644 index 2fec283..0000000 Binary files a/docs/images/estimated_ao_shade.png and /dev/null differ diff --git a/docs/images/flat_shade.png b/docs/images/flat_shade.png deleted file mode 100644 index 3c7a3a1..0000000 Binary files a/docs/images/flat_shade.png and /dev/null differ diff --git a/docs/images/global_normal_map_shade.png b/docs/images/global_normal_map_shade.png deleted file mode 100644 index edaf768..0000000 Binary files a/docs/images/global_normal_map_shade.png and /dev/null differ diff --git a/docs/images/gouraud_segregated_shade.png b/docs/images/gouraud_segregated_shade.png deleted file mode 100644 index 9b94d30..0000000 Binary files a/docs/images/gouraud_segregated_shade.png and /dev/null differ diff --git a/docs/images/light_z_normalmap_shader.png b/docs/images/light_z_normalmap_shader.png deleted file mode 100644 index b367bf4..0000000 Binary files a/docs/images/light_z_normalmap_shader.png and /dev/null differ diff --git a/docs/images/light_z_normalmap_shader_diffuse.png b/docs/images/light_z_normalmap_shader_diffuse.png deleted file mode 100644 index ed91705..0000000 Binary files a/docs/images/light_z_normalmap_shader_diffuse.png and /dev/null differ diff --git a/docs/images/shadow_buffer.png b/docs/images/shadow_buffer.png deleted file mode 100644 index 0fa5268..0000000 Binary files a/docs/images/shadow_buffer.png and /dev/null differ diff --git a/docs/images/shadow_shade.png b/docs/images/shadow_shade.png deleted file mode 100644 index ee71ccd..0000000 Binary files a/docs/images/shadow_shade.png and /dev/null differ diff --git a/docs/images/specular_map_shading.png b/docs/images/specular_map_shading.png deleted file mode 100644 index 6721a80..0000000 Binary files a/docs/images/specular_map_shading.png and /dev/null differ diff --git a/docs/images/tiny_shader.png b/docs/images/tiny_shader.png deleted file mode 100644 index f3ceedb..0000000 Binary files a/docs/images/tiny_shader.png and /dev/null differ diff --git a/docs/images/wrong_normal_input_use_vertex_point_as_normal_direction.png b/docs/images/wrong_normal_input_use_vertex_point_as_normal_direction.png deleted file mode 100644 index 9cc1d8a..0000000 Binary files a/docs/images/wrong_normal_input_use_vertex_point_as_normal_direction.png and /dev/null differ diff --git a/geom.py b/geom.py index c766d0b..36f8307 100644 --- a/geom.py +++ b/geom.py @@ -6,11 +6,9 @@ from collections.abc import Iterable from itertools import chain -from functools import reduce import operator -from math import sqrt - +import math import numpy as np class Vector4DType(Enum): @@ -40,7 +38,7 @@ def __new__(cls, *args, shape: tuple = None): # pylint: disable=unused-argument return super().__new__(cls, list(args)) # Overwrite __init__ to add 'shape' keyword parameter - def __init__(self, *args, shape: tuple = None): # pylint: disable=unused-argument + def __init__(self, shape: tuple = None): if not shape is None: self._shape = shape @@ -48,42 +46,46 @@ def __init__(self, *args, shape: tuple = None): # pylint: disable=unused-argumen raise ShapeMissmatchException def __add__(self, other): - if type(self) == type(other): - (elems, _) = mat_add(self.get_field_values(), self._shape, + if other.__class__.__name__ == self.__class__.__name__: + (elems, _) = matadd(self.get_field_values(), self._shape, other.get_field_values(), other._shape) - return type(self)(*elems) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) else: raise TypeError def __sub__(self, other): - if type(self) == type(other): - (elems, _) = mat_sub(self.get_field_values(), self._shape, + if other.__class__.__name__ == self.__class__.__name__: + (elems, _) = matsub(self.get_field_values(), self._shape, other.get_field_values(), other._shape) - return type(self)(*elems) - - raise TypeError + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + else: + raise TypeError def __mul__(self, other): - if isinstance(other, (float, int)): - (elems, _) = comp_mul(self.get_field_values(), self._shape, other) - return type(self)(*elems) - - # All other cases should already have been handled in instance classes - raise TypeError + if other.__class__.__name__ in ["float", "int"]: + (elems, _) = compmul(self.get_field_values(), self._shape, other) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + else: + raise TypeError def __rmul__(self, other): - if isinstance(other, (float, int)): - (elems, _) = comp_mul(self.get_field_values(), self._shape, other) - return type(self)(*elems) - - raise TypeError + if other.__class__.__name__ in ["float", "int"]: + (elems, _) = compmul(self.get_field_values(), self._shape, other) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + else: + raise TypeError def __truediv__(self, other): - if isinstance(other, (float, int)): - (elems, _) = comp_div(self.get_field_values(), self._shape, other) - return type(self)(*elems) - - raise TypeError + if other.__class__.__name__ in ["float", "int"]: + (elems, _) = compdiv(self.get_field_values(), self._shape, other) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + else: + raise TypeError def get_field_values(self): """Returns all field values of the typing.NamedTuple._asdict method as list. @@ -99,99 +101,101 @@ def __str__(self): npa = np.array(self).reshape(self._shape) return prefix + np.array2string(npa, prefix=prefix) + ")" + +class MixinMatrix(MixinAlgebra): + """Mixin providing additional functionalty for matrices based on typing.NamedTuple.""" + def __mul__(self, other): + if MixinVector in other.__class__.__bases__: + (elems, shp) = matmul(self.get_field_values(), self._shape, + other.get_field_values(), other._shape) + if self.is_square(): + cl_type = globals()[other.__class__.__name__] + return cl_type(*elems) + else: + return Matrix_NxN(elems, shape = shp) + + elif MixinMatrix in other.__class__.__bases__: + (elems, shp) = matmul(self.get_field_values(), self._shape, + other.get_field_values(), other._shape) + + if self.is_square() and other.is_square(): + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + else: + return Matrix_NxN(elems, shape = shp) + + def is_square(self): + """Returns true if the matrix has square shape e.g. 2x2, 3x3, 5x5 matrices.""" + return self._shape[0] == self._shape[1] + + def inv(self): + """Returns inverse of a matrix.""" + (elems, _) = inverse(self, self._shape) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + + def tr(self): + """Returns transpose of a matrix.""" + (elems, shape) = transpose(self.get_field_values(), self._shape) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems, shape = shape) + def get_row(self, row_idx): """Returns content of row as MatrixNxN object.""" (rows, cols) = self._shape elems = self.get_field_values() start_idx = row_idx * rows - shp = (1, cols) - cl_type = get_standard_type(shp) - return cl_type(*elems[start_idx:start_idx+cols], shape = (1, cols)) + return Matrix_NxN(elems[start_idx:start_idx+cols], shape = (1, cols)) def get_col(self, col_idx): """Returns content of column as MatrixNxN oject.""" - # Fixme: Improve speed here. Too many transposes return self.tr().get_row(col_idx) - def set_row(self, row_idx, other): + def set_row(self, row_idx, other: Iterable): """Returns same object type with replaced row content.""" (rows, cols) = self._shape - if isinstance(other, Iterable): - lst = unpack_nested_iterable_to_list(other) - else: - lst = [other] + li = unpack_nested_iterable_to_list(other) - if len(lst) == cols and row_idx < rows: + if len(li) == cols and row_idx < rows: elems = self.get_field_values() start_idx = row_idx * cols - elems[start_idx:start_idx+cols] = lst - return type(self)(elems, shape = self._shape) - - raise ShapeMissmatchException + elems[start_idx:start_idx+cols] = li + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems, shape = self._shape) + else: + raise ShapeMissmatchException def set_col(self, col_idx, other: Iterable): """Returns same object type with replaced col content.""" return self.tr().set_row(col_idx, other).tr() - -class MixinMatrix(MixinAlgebra): - """Mixin providing additional functionalty for matrices based on typing.NamedTuple.""" - def __mul__(self, other): - if isinstance(other, (MixinMatrix, MixinVector)): - (elems, shp) = matmul(self.get_field_values(), self._shape, - other.get_field_values(), other._shape) - return get_standard_type(shp)(*elems, shape = shp) - - # Fallback to more common MixinAlgebra __mul__ - return super().__mul__(other) - - def is_square(self): - """Returns true if the matrix has square shape e.g. 2x2, 3x3, 5x5 matrices.""" - return self._shape[0] == self._shape[1] - - def inv(self): - """Returns inverse of a matrix.""" - (elems, _) = inverse(self, self._shape) - return type(self)(*elems) - - def tr(self): # pylint: disable=invalid-name - """Returns transpose of a matrix.""" - (elems, shape) = transpose(self.get_field_values(), self._shape) - return type(self)(elems, shape = shape) - class MixinVector(MixinAlgebra): """Mixin providing additional functionalty for vectors based on typing.NamedTuple.""" def __mul__(self, other): - if isinstance(self, MixinVector) and isinstance(other, MixinVector): - if self._shape[0] > other._shape[0]: - raise ShapeMissmatchException - else: - # Calc scalar product - (elems, _) = matmul(self.get_field_values(), self._shape, - other.get_field_values(), other._shape) + if self.__class__.__name__ == other.__class__.__name__ and \ + self._shape[0] < other._shape[0]: + # Calc scalar product + (elems, _) = matmul(self.get_field_values(), self._shape, + other.get_field_values(), other._shape) return elems[0] - # Fallback to MixinAlgebra __mul__ - return super().__mul__(other) + elif other.__class__.__name__ in ["float", "int"]: + return super().__mul__(other) def __floordiv__(self, other): - if isinstance(other, (float, int)): - (elems, _) = comp_floor(self.get_field_values(), self._shape, other) - return type(self)(elems, shape = self._shape) - - return ValueError + if other.__class__.__name__ in ["float", "int"]: + (elems, _) = compfloor(self.get_field_values(), self._shape, other) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems, shape = self._shape) - def tr(self): # pylint: disable=invalid-name + def tr(self): """Returns a transposed vector.""" # Transpose MixinVector (rows, cols) = self._shape - - elems = self._asdict().values() - return type(self)(elems, shape = (cols, rows)) - def abs(self): - """Returns length of vector.""" - return vect_norm(self.get_field_values()) + cl_type = globals()[self.__class__.__name__] + elems = self._asdict().values() + return cl_type(*elems, shape = (cols, rows)) class Point2D(MixinVector, metaclass=NamedTupleMetaEx): """Two-dimensional point with x and y ordinate.""" @@ -211,12 +215,6 @@ class Barycentric(MixinVector, metaclass=NamedTupleMetaEx): one_u_v: float u: float v: float - -class Vector2D(MixinVector, metaclass=NamedTupleMetaEx): - """Two-dimensional point with x and y ordinate.""" - _shape = (2,1) - x: float - y: float class Vector3D(MixinVector, metaclass=NamedTupleMetaEx): """Three-dimensional vector with x, y, z component.""" _shape = (3,1) @@ -224,7 +222,7 @@ class Vector3D(MixinVector, metaclass=NamedTupleMetaEx): y: float z: float - def expand_4D(self, vtype): # pylint: disable=invalid-name + def expand_4D(self, vtype): """Expands 3D vector to 4D vector regarding the given type. Options: @@ -232,19 +230,27 @@ def expand_4D(self, vtype): # pylint: disable=invalid-name Vector4D(x, y, z, 1) for vectors identifying a vertex in 3D space. """ - new_shape = (4,1) if is_col_vect(self._shape) else (1,4) + if is_col_vect(self._shape): + new_shape = (4,1) + else: + new_shape = (1,4) if vtype == Vector4DType.DIRECTION: return Vector4D(self.x, self.y, self.z, 0, shape = new_shape) - if vtype == Vector4DType.POINT: + elif vtype == Vector4DType.POINT: return Vector4D(self.x, self.y, self.z, 1, shape = new_shape) - return ValueError + def abs(self): + """Returns length of vector.""" + return math.sqrt(self.x**2 + self.y**2 + self.z**2) def normalize(self): """Normalizes vector to length = 1.0.""" abl = self.abs() - return self / abl if abl > 0 else None + if abl > 0: + return self / abl + else: + return None class Vector4D(MixinVector, metaclass=NamedTupleMetaEx): """Four-dimensional vector with x, y, y and a component.""" @@ -255,7 +261,7 @@ class Vector4D(MixinVector, metaclass=NamedTupleMetaEx): z: float a: float - def project_3D(self, vtype): # pylint: disable=invalid-name + def project_3D(self, vtype): """Reduces four-dimensional vector to three dimensions. If vector has directional meaning the 'a' component is just omited. @@ -263,16 +269,19 @@ def project_3D(self, vtype): # pylint: disable=invalid-name by diving all components through last component 'a'. """ - new_shape = (3,1) if is_col_vect(self._shape) else (1,3) + if is_col_vect(self._shape): + new_shape = (3,1) + else: + new_shape = (1,3) if vtype == Vector4DType.DIRECTION: return Vector3D(self.x, self.y, self.z, shape = new_shape) - if vtype == Vector4DType.POINT: + elif vtype == Vector4DType.POINT: return Vector3D(self.x / self.a, self.y / self.a, self.z / self.a, shape = new_shape) + else: + raise ValueError - raise ValueError - -class MatrixNxN(MixinMatrix, metaclass=NamedTupleMetaEx): +class Matrix_NxN(MixinMatrix, metaclass=NamedTupleMetaEx): """Matrix with any size (n x n). Parameters: @@ -287,12 +296,12 @@ class MatrixNxN(MixinMatrix, metaclass=NamedTupleMetaEx): class MatrixUV(MixinMatrix, metaclass=NamedTupleMetaEx): """Matrix with size (2 x 3) holding three pairs of uv coordinates.""" _shape = (2,3) - u_0: float - u_1: float - u_2: float - v_0: float - v_1: float - v_2: float + u0: float + u1: float + u2: float + v0: float + v1: float + v2: float class Matrix3D(MixinMatrix, metaclass=NamedTupleMetaEx): """Three-dimensional square matrix.""" @@ -307,21 +316,6 @@ class Matrix3D(MixinMatrix, metaclass=NamedTupleMetaEx): a32: float a33: float -class ScreenCoords(MixinMatrix, metaclass=NamedTupleMetaEx): - """Three-dimensional square matrix for screen coords containing - three x,y,z vectors in three columns. - """ - _shape = (3, 3) - v_0_x: float - v_1_x: float - v_2_x: float - v_0_y: float - v_1_y: float - v_2_y: float - v_0_z: float - v_1_z: float - v_2_z: float - class Matrix4D(MixinMatrix, metaclass=NamedTupleMetaEx): """Four-dimensional square matrix.""" _shape = (4, 4) @@ -407,42 +401,30 @@ def cross_product(v_0: Vector3D, v_1: Vector3D): c_2 = v_0.x * v_1.y - v_0.y * v_1.x return Vector3D(c_0, c_1, c_2) -def comp_min(v_0, v_1): - """Componentwise min function. Returns min vector.""" - return Vector3D(min(v_0.x, v_1.x), min(v_0.y, v_1.y), min(v_0.z, v_1.z)) - -def comp_max(v_0, v_1): - """Componentwise max function. Returns max vector.""" - return Vector3D(max(v_0.x, v_1.x), max(v_0.y, v_1.y), max(v_0.z, v_1.z)) +def comp_min(v0, v1): + return Vector3D(min(v0.x, v1.x), min(v0.y, v1.y), min(v0.z, v1.z)) -def transform_vertex_to_screen(v : Vector3D, M: Matrix4D): # pylint: disable=invalid-name - """Transforms 3D vertex to screen coordinates. - Usually at least viewport matrix is passed for this step as matrix M. - - Returns 3D vector containing int screen coordinates x,y and float z component. - """ +def comp_max(v0, v1): + return Vector3D(max(v0.x, v1.x), max(v0.y, v1.y), max(v0.z, v1.z)) +def transform_vertex_to_screen(v : Vector3D, M: Matrix4D): v = transform_3D4D3D(v, Vector4DType.POINT, M) - v_z = v.z + vz = v.z v = v // 1 - return Vector3D(v.x, v.y, v_z) + return Vector3D(v.x, v.y, vz) -def transform_3D4D3D(vert: Vector3D, vtype: Vector4DType, M: Matrix4D): # pylint: disable=invalid-name - """Transforms 3D vertex with matrix. Projects vector to screen plane - if vector type is point (dividing by a component of internal 4D vector). - """ - vert = M * vert.expand_4D(vtype) - return vert.project_3D(vtype) +def transform_3D4D3D(v: Vector3D, vtype: Vector4DType, M: Matrix4D): + v = M * v.expand_4D(vtype) + return v.project_3D(vtype) -def unpack_nested_iterable_to_list(it_er: Iterable): - """Unpacks nested iterables. e.g. [[1,2], [3,4]] becomes [1,2,3,4].""" - while any(isinstance(e, Iterable) for e in it_er): +def unpack_nested_iterable_to_list(it: Iterable): + while any(isinstance(e, Iterable) for e in it): # An iterable is nested in the parent iterable - it_er = list(chain.from_iterable(it_er)) - return it_er + it = list(chain.from_iterable(it)) + return it + +def compmul(mat_0: list, shape_0: tuple, c: float): -def comp_mul(mat_0: list, shape_0: tuple, factor: float): - """Performing componentwise multiplication with factor c.""" (rows_0, cols_0) = shape_0 if len(mat_0) != (rows_0 * cols_0): @@ -450,10 +432,10 @@ def comp_mul(mat_0: list, shape_0: tuple, factor: float): raise ShapeMissmatchException else: # Return coefficients and shape tuple - return [e * factor for e in mat_0], shape_0 + return [e * c for e in mat_0], shape_0 -def comp_div(mat_0: list, shape_0: tuple, divisor: float): - """Performing componentwise real division by divisor.""" +def compdiv(mat_0: list, shape_0: tuple, c: float): + """Performing componentwise real division.""" (rows_0, cols_0) = shape_0 if len(mat_0) != (rows_0 * cols_0): @@ -461,9 +443,9 @@ def comp_div(mat_0: list, shape_0: tuple, divisor: float): raise ShapeMissmatchException else: # Return coefficients and shape tuple - return [e / divisor for e in mat_0], shape_0 + return [e / c for e in mat_0], shape_0 -def comp_floor(mat_0: list, shape_0: tuple, divisor: float): +def compfloor(mat_0: list, shape_0: tuple, c: float): """Performing componentwise floor division.""" (rows_0, cols_0) = shape_0 @@ -472,15 +454,10 @@ def comp_floor(mat_0: list, shape_0: tuple, divisor: float): raise ShapeMissmatchException else: # Return coefficients and shape tuple - return [int(e // divisor) for e in mat_0], shape_0 + return [int(e // c) for e in mat_0], shape_0 -def vect_norm(all_elems: list): - """Return norm of n-dim vector.""" - squared = [elem**2 for elem in all_elems] - return sqrt(reduce(operator.add, squared)) +def matadd(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): -def mat_add(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): - """Performing componentwise addition.""" (rows_0, cols_0) = shape_0 (rows_1, cols_1) = shape_1 @@ -493,10 +470,9 @@ def mat_add(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): # Return coefficients and shape tuple return map(operator.add, mat_0, mat_1), shape_0 -def mat_sub(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): - """Performing componentwise substraction.""" +def matsub(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): mat_1 = [e * -1 for e in mat_1] - return mat_add(mat_0, shape_0, mat_1, shape_1) + return matadd(mat_0, shape_0, mat_1, shape_1) def is_row_vect(shape: tuple): """Returning true if vector shape is row space e.g. shape = (1,4)""" @@ -506,24 +482,5 @@ def is_col_vect(shape: tuple): """Returning true if vector shape is col space e.g. shape = (4,1)""" return shape[0] > shape[1] -def get_standard_type(shape: tuple): - """Return standard return classes for given shapes.""" - rows, cols = shape - if cols > rows: - # Switch to have shape sorted - shape = (cols, rows) - if shape == (4,4): - return Matrix4D - if shape == (3,3): - return Matrix3D - if shape == (4,1): - return Vector4D - if shape == (3,1): - return Vector3D - if shape == (2,1): - return Point2D - # Fallback to NxN Matrix if no special shape applies - return MatrixNxN - class ShapeMissmatchException(Exception): - """Exception raised when matrix, vector dimensions do not fit.""" + pass diff --git a/main.py b/main.py index 708e0f8..4f914f6 100644 --- a/main.py +++ b/main.py @@ -1,47 +1,81 @@ """A tiny shader fork written in Python 3""" -import progressbar + +from progressbar import progressbar from tiny_image import TinyImage -import our_gl as gl -from geom import ScreenCoords, Vector3D -from model import ModelStorage, NormalMapType -from tiny_shaders import TinyShader, DepthShader +import our_gl as gl +from geom import Vector_3D +from model import Model_Storage, NormalMapType +from tiny_shaders import FlatShader, GouraudShader, GouraudShaderSegregated, \ + DiffuseGouraudShader, GlobalNormalmapShader, SpecularmapShader, \ + TangentNormalmapShader if __name__ == "__main__": - # Model property definition - OBJ_FILENAME = "obj/autumn/autumn.obj" - DIFFUSE_FILENAME = "obj/autumn/TEX_autumn_body_color.tga" - NORMAL_MAP_FILENAME = "obj/autumn/TEX_autumn_body_normals_wrld.tga" - NORMAL_MAP_TYPE = NormalMapType.GLOBAL - SPECULAR_MAP_FILENAME = "obj/autumn/TEX_autumn_body_spec.tga" - AO_MAP_FILENAME = "obj/autumn/TEX_autumn_body_ao.tga" - OUTPUT_FILENAME = "renders/out.png" - - # Image property definition - (w, h) = (800, 800) + # Model property selection + MODEL_PROP_SET = 1 + if MODEL_PROP_SET == 0: + OBJ_FILENAME = "obj/autumn/autumn.obj" + DIFFUSE_FILENAME = "obj/autumn/TEX_autumn_body_color.png" + NORMAL_MAP_FILENAME = "obj/autumn/TEX_autumn_body_normals_wrld_space.tga" + NORMAL_MAP_TYPE = NormalMapType.GLOBAL + SPECULAR_MAP_FILENAME = "obj/autumn/TEX_autumn_body_spec.tga" + OUTPUT_FILENAME = "renders/out.png" + elif MODEL_PROP_SET == 1: + OBJ_FILENAME = "obj/head/head.obj" + DIFFUSE_FILENAME = "obj/head/head_diffuse.tga" + NORMAL_MAP_FILENAME = "obj/head/head_nm_tangent.tga" + NORMAL_MAP_TYPE = NormalMapType.TANGENT + SPECULAR_MAP_FILENAME = "obj/head/head_spec.tga" + OUTPUT_FILENAME = "renders/out.png" + else: + OBJ_FILENAME = "obj/head/head.obj" + DIFFUSE_FILENAME = "obj/head/head_diffuse.tga" + NORMAL_MAP_FILENAME = "obj/head/head_nm.tga" + NORMAL_MAP_TYPE = NormalMapType.GLOBAL + SPECULAR_MAP_FILENAME = "obj/head/head_spec.tga" + OUTPUT_FILENAME = "renders/out.png" + + # Image property selection + IMG_PROP_SET = 1 + if IMG_PROP_SET == 0: + (w, h) = (2000, 2000) + else: + (w, h) = (800, 800) + image = TinyImage(w, h) - # View property definition - VIEW_PROP_SET = 0 - EYE = Vector3D(0, 0, 4) # Lookat camera 'EYE' position - CENTER = Vector3D(0, 0, 0) # Lookat 'CENTER'. 'EYE' looks at CENTER - UP = Vector3D(0, 1, 0) # Camera 'UP' direction - SCALE = .8 # Viewport scaling - + # View property selection + VIEW_PROP_SET = 1 + if VIEW_PROP_SET == 0: + EYE = Vector_3D(0, 0, 1) # Lookat camera 'EYE' position + CENTER = Vector_3D(0, 0, 0) # Lookat 'CENTER'. 'EYE' looks at CENTER + UP = Vector_3D(0, 1, 0) # Camera 'UP' direction + SCALE = .8 # Viewport scaling + elif VIEW_PROP_SET == 1: + EYE = Vector_3D(1, 0, 1) + CENTER = Vector_3D(0, 0, 0) + UP = Vector_3D(0, 1, 0) + SCALE = .8 + else: + EYE = Vector_3D(1, 0, 0) # Lookat camera 'EYE' position + CENTER = Vector_3D(0, 0, 0) # Lookat 'CENTER'. 'EYE' looks at CENTER + UP = Vector_3D(0, 1, 0) # Camera 'UP' direction + SCALE = .8 # Viewport scaling + # Light property - LIGHT_DIR = Vector3D(1, 1, 1).normalize() + LIGHT_DIR = Vector_3D(1, 0, 1).norm() print("Reading modeldata ...") - mdl = ModelStorage(object_name = "autumn", obj_filename=OBJ_FILENAME, - diffuse_map_filename=DIFFUSE_FILENAME, - normal_map_filename=NORMAL_MAP_FILENAME, normal_map_type=NORMAL_MAP_TYPE, - specular_map_filename=SPECULAR_MAP_FILENAME, ao_map_filename=AO_MAP_FILENAME) + mdl = Model_Storage(object_name = "autumn", obj_filename=OBJ_FILENAME, + diffuse_map_filename=DIFFUSE_FILENAME, + normal_map_filename=NORMAL_MAP_FILENAME, normal_map_type=NORMAL_MAP_TYPE, + specular_map_filename=SPECULAR_MAP_FILENAME) # Define tranformation matrices # Generate model transformation matrix which transforms - # vertices according to the model bounding box + # # vertices according to the model bounding box # # min[-1, -1, -1] to max[1, 1, 1] object space M_model = gl.model_transform(mdl.bbox[0], mdl.bbox[1]) @@ -50,12 +84,10 @@ M_lookat = gl.lookat(EYE, CENTER, UP) # Generate perspective transformation - z_cam_dist = (EYE - CENTER).abs() - M_perspective = gl.perspective(z_cam_dist) + M_perspective = gl.perspective(4.0) # Generate transformation to final viewport - DEPTH_RES = 255 # [0 ... 255] - M_viewport = gl.viewport(+SCALE*w/8, +SCALE*h/8, SCALE*w, SCALE*h, DEPTH_RES) + M_viewport = gl.viewport(+SCALE*w/8, +SCALE*h/8, SCALE*w, SCALE*h, 255) # Combine matrices M_modelview = M_lookat * M_model @@ -64,59 +96,33 @@ M_pe_IT = M_pe.tr().inv() - # Init vars for normal shader run zbuffer = [[-float('Inf') for bx in range(w)] for y in range(h)] - screen_coords = ScreenCoords(9*[0]) - - # Init vars for shaders which use a shadow buffer - shadow_buffer = None - shadow_image = None - M_sb = None - - PREPARE_AO_SHADER = False - PREPARE_SHADOW_SHADER = False - - # Shader definition - PREPARE_SHADOW_SHADER = True - shader = TinyShader(mdl, LIGHT_DIR, M_pe, M_sc, M_pe_IT, None, None) - if PREPARE_SHADOW_SHADER: - # Fill shadow buffer and set data for final shader - - # Calculate shadow buffer matrices - M_lookat_cam_light = gl.lookat(LIGHT_DIR, CENTER, UP) - M_sb = M_viewport * M_lookat_cam_light * M_model - - shadow_buffer = [[-float('Inf') for bx in range(w)] for y in range(h)] - shadow_image = TinyImage(w, h) - - # Depth shader and shadow buffer - depth_shader = DepthShader(mdl, M_sb, DEPTH_RES) - - # Apply data to normal shader - shader.uniform_M_sb = M_sb - shader.shadow_buffer = shadow_buffer - - print("Saving shadow buffer ...") - for face_idx in progressbar.progressbar(range(mdl.get_face_count())): - for face_vert_idx in range(3): - # Get transformed vertex and prepare internal shader data - vert = depth_shader.vertex(face_idx, face_vert_idx) - screen_coords = screen_coords.set_col(face_vert_idx, vert) - - # Rasterize image (z heigth in dir of light). Shadow buffer is filles as well - shadow_image = gl.draw_triangle(screen_coords, depth_shader, shadow_buffer, - shadow_image) + SHADER_PROP_SET = 5 + if SHADER_PROP_SET == 0: + shader = GouraudShader(mdl, LIGHT_DIR, M_sc) + elif SHADER_PROP_SET == 1: + shader = GouraudShaderSegregated(mdl, LIGHT_DIR, M_sc, 4) + elif SHADER_PROP_SET == 2: + shader = DiffuseGouraudShader(mdl, LIGHT_DIR, M_sc) + elif SHADER_PROP_SET == 3: + shader = GlobalNormalmapShader(mdl, LIGHT_DIR, M_pe, M_sc, M_pe_IT) + elif SHADER_PROP_SET == 4: + shader = SpecularmapShader(mdl, LIGHT_DIR, M_pe, M_sc, M_pe_IT) + elif SHADER_PROP_SET == 5: + shader = TangentNormalmapShader(mdl, LIGHT_DIR, M_pe, M_pe_IT, M_viewport) + else: + shader = FlatShader(mdl, LIGHT_DIR, M_sc) + + # Iterate model faces + print("Drawing triangles ...") - shadow_image.save_to_disk("renders/shadow_buffer.png") + screen_coords = [None] * 3 - # Final shader run - print("Drawing triangles ...") - for face_idx in progressbar.progressbar(range(mdl.get_face_count())): + for face_idx in progressbar(range(mdl.get_face_count())): for face_vert_idx in range(3): # Get transformed vertex and prepare internal shader data - vert = shader.vertex(face_idx, face_vert_idx) - screen_coords = screen_coords.set_col(face_vert_idx, vert) + screen_coords[face_vert_idx] = shader.vertex(face_idx, face_vert_idx) # Rasterize triangle image = gl.draw_triangle(screen_coords, shader, zbuffer, image) diff --git a/model.py b/model.py index 950ba88..382ae81 100644 --- a/model.py +++ b/model.py @@ -1,10 +1,13 @@ -"""Module providing functionality for storing and reading .obj and texture map data.""" import re -from enum import Enum +import sys + +import our_gl as gl from collections import namedtuple from tiny_image import TinyImage -from geom import Vector3D, Point2D, PointUV, comp_min, comp_max +from numpy import array +from enum import Enum +from geom import Vector_3D, Point_2D, comp_min, comp_max VertexIds = namedtuple("VertexIds", "id_one id_two id_three") DiffusePointIds = namedtuple("DiffusePointIds", "id_one id_two id_three") @@ -12,12 +15,11 @@ FacedataIds = namedtuple("FacedataIds", "VertexIds DiffusePointIds NormalIds") class NormalMapType(Enum): - """Enum specifying normal map type: Global normals or tangent space normals.""" GLOBAL = 1 TANGENT = 2 def get_model_face_ids(obj_filename): - """Returns ids associated to a model's face: Vertex, normal and uv point ids.""" + face_line_pattern = r"^f" face_id_data_list = [] @@ -31,7 +33,7 @@ def get_model_face_ids(obj_filename): return face_id_data_list def read_face_ids(face_data_line): - """Returns ids associated to a face of a .obj dataline.""" + face_elem_pattern = r"(\d+)\/(\d*)\/(\d+)" match = re.findall(face_elem_pattern, face_data_line) @@ -42,7 +44,7 @@ def read_face_ids(face_data_line): for idx in range(0, len(match)): # Decrease all indices as .obj files are indexed starting at one vert_list.append(int(match[idx][0]) - 1) - + diffuse_point_id = match[idx][1] if diffuse_point_id.isdigit(): diffuse_point_id = int(diffuse_point_id) @@ -59,7 +61,7 @@ def read_face_ids(face_data_line): return FacedataIds(vert_ids, diffuse_pt_ids, norm_ids) def get_model_diffuse_points(obj_filename): - """Returns all uv points of .obj file.""" + coord_line_pattern = r"^vt" diffuse_point_list = [] @@ -67,25 +69,24 @@ def get_model_diffuse_points(obj_filename): for line in obj_file: match = re.search(coord_line_pattern, line) if match: - pt_two_d = read_diffuse_points(line) - diffuse_point_list.append(pt_two_d) + pt = read_diffuse_points(line) + diffuse_point_list.append(pt) return diffuse_point_list def read_diffuse_points(diffuse_data_line): - """Returns all points of a uv point dataline.""" + vertex_elem_pattern = r"[+-]?[0-9]*[.]?[0-9]+[e\+\-\d]*" match = re.findall(vertex_elem_pattern, diffuse_data_line) - return Point2D(float(match[0]), float(match[1])) # match[2] is not read + return Point_2D(float(match[0]), float(match[1])) # match[2] is not read def get_vertices(obj_filename): - """Returns all vertices of .obj file.""" vertex_list = [] vertex_pattern = r"^v\s" - bb_min = Vector3D(float('inf'), float('inf'), float('inf')) - bb_max = Vector3D(float('-inf'), float('-inf'), float('-inf')) + bb_min = Vector_3D(float('inf'), float('inf'), float('inf')) + bb_max = Vector_3D(float('-inf'), float('-inf'), float('-inf')) with open(obj_filename) as obj_file: for line in obj_file: @@ -99,7 +100,7 @@ def get_vertices(obj_filename): for elem in match: elem_list.append(float(elem)) - vert = Vector3D(*elem_list) + vert = Vector_3D(*elem_list) vertex_list.append(vert) bb_min = comp_min(vert, bb_min) @@ -108,7 +109,6 @@ def get_vertices(obj_filename): return vertex_list, (bb_min, bb_max) def get_normals(obj_filename): - """Returns all vertex normals of .obj file.""" normal_list = [] normal_pattern = r"^vn\s" @@ -125,14 +125,12 @@ def get_normals(obj_filename): for elem in match: elem_list.append(float(elem)) - normal = Vector3D(*elem_list).normalize() + normal = Vector_3D(*elem_list).norm() normal_list.append(normal) return normal_list -class ModelStorage(): - """Class storing model data.""" - object_name = "" +class Model_Storage(): face_id_data = [] vertices = [] normals = [] @@ -140,20 +138,20 @@ class ModelStorage(): diffuse_points = [] diffuse_map = None + normal_map_type = NormalMapType.GLOBAL normal_map = None + specular_map = None - ao_map = None - def __init__(self, object_name: str = None, obj_filename: str = None, - diffuse_map_filename: str = None, + def __init__(self, object_name: str = None, obj_filename: str = None, + diffuse_map_filename: str = None, normal_map_filename: str = None, normal_map_type = NormalMapType.GLOBAL, - specular_map_filename:str = None, ao_map_filename:str = None): + specular_map_filename:str = None): - self.object_name = object_name self.face_id_data = get_model_face_ids(obj_filename) (self.vertices, self.bbox) = get_vertices(obj_filename) - + # Load texture ('diffuse_map') if not diffuse_map_filename is None: self.diffuse_points = get_model_diffuse_points(obj_filename) @@ -166,67 +164,45 @@ def __init__(self, object_name: str = None, obj_filename: str = None, self.normals = get_normals(obj_filename) self.normal_map = TinyImage() self.normal_map.load_image(normal_map_filename) - + # Specular normal map if not specular_map_filename is None: self.specular_map = TinyImage() self.specular_map.load_image(specular_map_filename) - # Ambient occlusion map - if not ao_map_filename is None: - self.ao_map = TinyImage() - self.ao_map.load_image(ao_map_filename) - def get_normal(self, face_idx, face_vertex_idx): - """Returns face vertex normal.""" normal_idx = self.face_id_data[face_idx].NormalIds[face_vertex_idx] - return self.normals[normal_idx].normalize() + return self.normals[normal_idx] def get_vertex(self, face_idx, face_vertex_idx): - """Returns face vertex.""" vertex_idx = self.face_id_data[face_idx].VertexIds[face_vertex_idx] return self.vertices[vertex_idx] def get_uv_map_point(self, face_idx, face_vertex_idx): - """Returns uv map point.""" diffuse_idx = self.face_id_data[face_idx].DiffusePointIds[face_vertex_idx] return self.diffuse_points[diffuse_idx] - def get_diffuse_color(self, pnt: PointUV): - """Returns diffuse color from texture map.""" - # Make sure to only use RGB components in Vector3D - return Vector3D(self.diffuse_map.get(int(pnt.u * self.diffuse_map.get_width()), - int(pnt.v * self.diffuse_map.get_height()))[:3]) - - def get_normal_from_map(self, pnt: PointUV): - """Returns normal from model normalmap.""" - # Make sure to only use RGB components in Vector3D - rgb = Vector3D(*self.normal_map.get(int(pnt.u * self.normal_map.get_width()), - int(pnt.v * self.normal_map.get_height()))[:3]) - return (rgb / 255 * 2 - Vector3D(1, 1, 1)).normalize() - - def get_specular_power_from_map(self, pnt: PointUV): - """Returns specular power coefficient from specular map.""" + def get_diffuse_color(self, rel_x, rel_y): + # Make sure to only use RGB components in Vector_3D + return Vector_3D(*self.diffuse_map.get(int(rel_x * self.diffuse_map.get_width()), + int(rel_y * self.diffuse_map.get_height()))[:3]) + + def get_normal_from_map(self, rel_x, rel_y): + # Make sure to only use RGB components in Vector_3D + rgb = Vector_3D(*self.normal_map.get(int(rel_x * self.normal_map.get_width()), + int(rel_y * self.normal_map.get_height()))[:3]) + return (rgb / 255 * 2 - Vector_3D(1, 1, 1)).norm() + + def get_specular_power_from_map(self, rel_x, rel_y): # Make sure to only use GRAY component - comp = self.specular_map.get(int(pnt.u * self.specular_map.get_width()), - int(pnt.v * self.specular_map.get_height())) + comp = self.specular_map.get(int(rel_x * self.specular_map.get_width()), + int(rel_y * self.specular_map.get_height())) if comp is tuple: comp = comp[0] - return comp - - def get_ao_intensity_from_map(self, pnt: PointUV): - """Returns ao_intensity from ao map.""" - # Make sure to only use GRAY component - comp = self.ao_map.get(int(pnt.u * self.ao_map.get_width()), - int(pnt.v * self.ao_map.get_height())) - if comp is tuple: - comp = comp[0] - return comp / 255.0 - + return comp + def get_vertex_count(self): - """Returns count of model vertices.""" return len(self.vertices) def get_face_count(self): - """Returns count of model faces.""" - return len(self.face_id_data) + return len(self.face_id_data) \ No newline at end of file diff --git a/obj/autumn/TEX_autumn_body_ao.tga b/obj/autumn/TEX_autumn_body_ao.tga deleted file mode 100644 index da34d79..0000000 Binary files a/obj/autumn/TEX_autumn_body_ao.tga and /dev/null differ diff --git a/obj/autumn/TEX_autumn_body_color.png b/obj/autumn/TEX_autumn_body_color.png new file mode 100644 index 0000000..bd9889a Binary files /dev/null and b/obj/autumn/TEX_autumn_body_color.png differ diff --git a/obj/autumn/TEX_autumn_body_color.tga b/obj/autumn/TEX_autumn_body_color.tga deleted file mode 100644 index de3a9ce..0000000 Binary files a/obj/autumn/TEX_autumn_body_color.tga and /dev/null differ diff --git a/obj/autumn/TEX_autumn_body_normals_tngt.tga b/obj/autumn/TEX_autumn_body_normals_tngt.tga deleted file mode 100644 index 9ad751d..0000000 Binary files a/obj/autumn/TEX_autumn_body_normals_tngt.tga and /dev/null differ diff --git a/obj/autumn/TEX_autumn_body_normals_wrld.tga b/obj/autumn/TEX_autumn_body_normals_wrld_space.tga similarity index 100% rename from obj/autumn/TEX_autumn_body_normals_wrld.tga rename to obj/autumn/TEX_autumn_body_normals_wrld_space.tga diff --git a/obj/autumn/autumn.obj b/obj/autumn/autumn.obj index 30e8c8d..8b5423a 100644 --- a/obj/autumn/autumn.obj +++ b/obj/autumn/autumn.obj @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ea428d859fdba386d7c41a3df610b6342afaf763887616a5cb4e9ec8f6f5de1 -size 490120 +oid sha256:529afe34b40cf399907c52e159625cd12aab1ef517c310f7a9e9e38bf7e21e97 +size 490126 diff --git a/obj/debug/grid.tga b/obj/debug/grid.tga deleted file mode 100644 index 2dac889..0000000 Binary files a/obj/debug/grid.tga and /dev/null differ diff --git a/obj/debug/neutral_tangent.tga b/obj/debug/neutral_tangent.tga deleted file mode 100644 index 04dedf3..0000000 Binary files a/obj/debug/neutral_tangent.tga and /dev/null differ diff --git a/our_gl.py b/our_gl.py index 996b461..235749a 100644 --- a/our_gl.py +++ b/our_gl.py @@ -1,196 +1,141 @@ -"""our_gl module mimics OpenGL functionality. Specifiy shader implementations - have to be made externally based on provided abstract class Shader. -""" +from operator import attrgetter from abc import ABC, abstractmethod from tiny_image import TinyImage -from geom import Matrix4D, ScreenCoords, Vector3D, Barycentric, Point2D, cross_product +from geom import Matrix_4D, Vector_3D, Point_2D, cross_product class Shader(ABC): - """Abstract class for tiny shaders.""" @abstractmethod - def vertex(self, face_idx: int, vert_idx: int): - """Vertex shader modifies vertices of mesh. Returns four-dimensional vector.""" + def vertex(self, face_idx: int, vert_idx: int): # Returns Vector_4D + pass @abstractmethod - def fragment(self, bary: Barycentric): # Returns bool and color - """Fragment shader modifies single image pixels. Returns color.""" + def fragment(self, barycentric: tuple): # Returns bool and color + pass -def draw_line(p_0, p_1, image, color): - """Draw p_0 line onto an image.""" +def draw_line(p0, p1, image, color): + """Draw p0 line onto an image.""" - (x_0, y_0) = p_0 - (x_1, y_1) = p_1 + (x0, y0) = p0 + (x1, y1) = p1 - if abs(x_1-x_0) < abs(y_1-y_0): + if abs(x1-x0) < abs(y1-y0): # Swap to prevent whitespace when y distance is higher than x distance (steep line) steep_line = True - (y_1, y_0, x_1, x_0) = (x_1, x_0, y_1, y_0) + (y1, y0, x1, x0) = (x1, x0, y1, y0) else: steep_line = False - if x_0 == x_1: - # Due to switching steep lines this only occurs if y_0 == y_1 and x_0 == x_1 - # Only draw p_0 dot in this case - image.set(x_0, y_0, color) + if x0 == x1: + # Due to switching steep lines this only occurs if y0 == y1 and x0 == x1 + # Only draw p0 dot in this case + image.set(x0, y0, color) return image - elif x_0 > x_1: - (y_1, y_0, x_1, x_0) = (y_0, y_1, x_0, x_1) + elif x0 > x1: + (y1, y0, x1, x0) = (y0, y1, x0, x1) - for x_sweep in range(x_0, x_1+1): - y_sweep = int(y_0 + (y_1 - y_0) / (x_1 - x_0) * (x_sweep - x_0) + .5) + for x in range(x0, x1+1): + #ToDo: Optimize speed using non float operations + y = int(y0 + (y1-y0) / (x1-x0) * (x-x0) + .5) if steep_line: - image.set(y_sweep, x_sweep, color) + image.set(y, x, color) else: - image.set(x_sweep, y_sweep, color) + image.set(x, y, color) return image -def draw_triangle_edges(p_0, p_1, p_2, image, color): - """Draws triangle lines for three given points onto image.""" - image = draw_line(p_0, p_1, image, color) - image = draw_line(p_1, p_2, image, color) - image = draw_line(p_2, p_0, image, color) +def draw_triangle_edges(p0, p1, p2, image, color): + image = draw_line(p0, p1, image, color) + image = draw_line(p1, p2, image, color) + image = draw_line(p2, p0, image, color) return image -def draw_triangle(screen_coords: ScreenCoords, shader: Shader, zbuffer: list, image: TinyImage): - """Base method of rasterizer which calls fragment shader.""" - # Read x component vector and get x min and max to draw in (framed by image size) - x_row = screen_coords.get_row(0) - x_min = get_min_in_frame(0, x_row) - x_max = get_max_in_frame(image.get_width() - 1, x_row) - # Read y component vector and get y min and max to draw in (framed by image size) - y_row = screen_coords.get_row(1) - y_min = get_min_in_frame(0, y_row) - y_max = get_max_in_frame(image.get_width() - 1, y_row) +def draw_triangle(screen_coords: list, shader: Shader, zbuffer: list, image: TinyImage): + temp_coords = screen_coords.copy() + temp_coords.sort(key=attrgetter('x')) + x_min = int(min(max(temp_coords[0].x, 0), image.width - 1)) + x_max = int(min(max(temp_coords[2].x, 0), image.width - 1)) - p_0 = Point2D(screen_coords.v_0_x, screen_coords.v_0_y) - p_1 = Point2D(screen_coords.v_1_x, screen_coords.v_1_y) - p_2 = Point2D(screen_coords.v_2_x, screen_coords.v_2_y) + temp_coords.sort(key=attrgetter('y')) + y_min = int(min(max(temp_coords[0].y, 0), image.height - 1)) + y_max = int(min(max(temp_coords[2].y, 0), image.height - 1)) - z_row = Vector3D(screen_coords.get_row(2), shape = (1,3)) + for x in range(x_min, x_max): + for y in range(y_min, y_max): + bary = barycentric(Point_2D(screen_coords[0].x, screen_coords[0].y), + Point_2D(screen_coords[1].x, screen_coords[1].y), + Point_2D(screen_coords[2].x, screen_coords[2].y), Point_2D(x,y)) - for image_x in range(x_min, x_max): - for image_y in range(y_min, y_max): - p_raster = Point2D(image_x, image_y) - bary = calc_barycentric(p_0, p_1, p_2 ,p_raster) + (one_uv, u, v) = bary - #(one_uv_bary, u_b, v_b) = bary - - if all([comp >= 0 for comp in bary]): - z_screen = z_row * bary - - if z_screen > zbuffer[image_x][image_y]: - zbuffer[image_x][image_y] = z_screen + if one_uv >= 0 and u >= 0 and v >= 0: + z = one_uv*screen_coords[0].z + u * screen_coords[1].z + v * screen_coords[2].z + + if z > zbuffer[x][y]: + zbuffer[x][y] = z discard, color = shader.fragment(bary) if not discard: - image.set(image_x, image_y, color) + image.set(x, y, color) return image -def calc_barycentric(p_0: Point2D, p_1: Point2D, p_2: Point2D, p_tri: Point2D): - """Returns barycentric coordinates for three given triangle points and fourth point - located relative to the triangle points. - """ - - bary_cross = cross_product(\ - Vector3D(p_1.x - p_0.x, p_2.x - p_0.x, p_0.x - p_tri.x), \ - Vector3D(p_1.y - p_0.y, p_2.y - p_0.y, p_0.y - p_tri.y)) - - (u_b, v_b, r_b) = bary_cross.x, bary_cross.y, bary_cross.z - - if r_b == 0: +def barycentric(p0:Point_2D, p1:Point_2D, p2:Point_2D, P:Point_2D): + (u, v, r) = cross_product(Vector_3D(p1.x-p0.x, p2.x-p0.x, p0.x-P.x), Vector_3D(p1.y-p0.y, p2.y-p0.y, p0.y-P.y)) + + if r == 0: # Triangle is degenerated - return Barycentric(-1, -1, -1) - - # Component r_b should be 1: Normalize components - return Barycentric(1 - (u_b + v_b) / r_b, - u_b / r_b , - v_b / r_b ) - -def model_transform(bounds_min: Vector3D, bounds_max: Vector3D): - """Returns transformation matrix of model. .obj data is scaled and offset to - appear to be x, y, z element of [-1, 1]. - """ + return (-1,-1,-1) + else: + # Component r should be 1: Normalize components + return (1-(u+v)/r, u/r, v/r) +def model_transform(bounds_min: Vector_3D, bounds_max: Vector_3D): bounds_delta = bounds_max - bounds_min bounds_scale = 2.0 / max(bounds_delta.x, bounds_delta.y, bounds_delta.z) bounds_offset = (bounds_max + bounds_min) * bounds_scale / 2 - M_model = Matrix4D([[bounds_scale, 0, 0, -bounds_offset.x], # pylint: disable=invalid-name - [0, bounds_scale, 0, -bounds_offset.y], - [0, 0, bounds_scale, -bounds_offset.z], - [0, 0, 0, 1 ]]) + M_model = Matrix_4D([[bounds_scale, 0, 0, -bounds_offset.x], + [0, bounds_scale, 0, -bounds_offset.y], + [0, 0, bounds_scale, -bounds_offset.z], + [0, 0, 0, 1 ]]) return M_model -def lookat(eye: Vector3D, center: Vector3D, up_dir: Vector3D): - """Returns transformatoin matrix for view (OpenGl gl_ulookat).""" - - z_v = (eye - center).normalize() - x_v = cross_product(up_dir, z_v).normalize() - y_v = cross_product(z_v, x_v).normalize() +def lookat(eye: Vector_3D, center: Vector_3D, up: Vector_3D): + z = (eye - center).norm() + x = cross_product(up, z).norm() + y = cross_product(z, x).norm() - M_inv = Matrix4D([[x_v.x, x_v.y, x_v.z, 0], # pylint: disable=invalid-name - [y_v.x, y_v.y, y_v.z, 0], - [z_v.x, z_v.y, z_v.z, 0], - [0 , 0 , 0 , 1]]) + M_inv = Matrix_4D([[x.x, x.y, x.z, 0], + [y.x, y.y, y.z, 0], + [z.x, z.y, z.z, 0], + [0 , 0 , 0 , 1]]) - M_tr = Matrix4D([[1, 0, 0, -center.x], # pylint: disable=invalid-name - [0, 1, 0, -center.y], - [0, 0, 1, -center.z], - [0, 0, 0, 1 ]]) + M_tr = Matrix_4D([[1, 0, 0, -center.x], + [0, 1, 0, -center.y], + [0, 0, 1, -center.z], + [0, 0, 0, 1 ]]) - M_lookat = M_inv * M_tr # pylint: disable=invalid-name + M_lookat = M_inv * M_tr return M_lookat -def perspective(z_dist: float): - """Returns transformation matrix for perspective transformation.""" - M_perspective = Matrix4D([[1, 0, 0 , 0], # pylint: disable=invalid-name - [0, 1, 0 , 0], - [0, 0, 1 , 0], - [0, 0, -1/z_dist, 1]]) - +def perspective(c: float): + M_perspective = Matrix_4D([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, -1/c, 1]]) + return M_perspective -def viewport(o_x, o_y, img_width, img_height, z_spread): - """Returns viewport transformation. Transforms vertices to screen coordinates.""" - - M_viewport = Matrix4D([[img_width/2, 0 , 0 , o_x + img_width/2 ], # pylint: disable=invalid-name - [0 , img_height/2, 0 , o_y + img_height/2], - [0 , 0 , z_spread/2, z_spread/2 ], - [0 , 0 , 0 , 1 ]]) +def viewport(o_x, o_y, w, h, d): + M_viewport = Matrix_4D([[w/2, 0, 0 , o_x + w/2], + [0 , h/2, 0 , o_y + h/2], + [0 , 0, d/2, d/2 ], + [0 , 0, 0 , 1 ]]) return M_viewport -def normal_transformation(M_transform: Matrix4D): # pylint: disable=invalid-name - """Transformation matrix for normal vectors. Retunrs inversed transpose - of transformatoin matrix.""" - - return M_transform.tr().inv() - -def get_min_in_frame(frm_lower_bnd, elems): - """Returns max value in frame: - - Examples: - e.g. frm_lower_bnd|..x_1....x2..x3| - get_min_in_frame = x1 - e.g. x3...frm_lower_bnd|..x_1....x2....| - get_min_in_frame = frm_lower_bnd - """ - min_in_frame = min(elems) - return max(frm_lower_bnd, min_in_frame) - -def get_max_in_frame(frm_upper_bnd, elems): - """Returns max value in frame: - - Examples: - e.g. |..x_1....x2..x3|frm_upper_bnd - get_max_in_frame = x3 - e.g. |..x_1....x2....|frm_upper_bnd..x3 - get_max_in_frame = frm_upper_bnd - """ - max_in_frame = max(elems) - return min(frm_upper_bnd, max_in_frame) +def normal_transformation(M_transform: Matrix_4D): + return M_transform.tr().inv() \ No newline at end of file diff --git a/tiny_image.py b/tiny_image.py index 4aba6db..fee290b 100644 --- a/tiny_image.py +++ b/tiny_image.py @@ -1,46 +1,37 @@ -"""tiny_image module for TinyImage class used in tiny_renderer.""" from PIL import Image from PIL import ImageDraw class TinyImage: - """ - This is the TinyImage class. - - Examples: - new_image = TinyImage(100, 100) - new_image = TinyImage().load_image("path/to/image/tiny.png") - new_image = new_image.set(x = 10, y = 100, color = 'white') - """ - - _im: Image - _draw: ImageDraw + """Get a new image canvas to draw on.""" def __init__(self, width = None, height = None): - if not width is None and not height is None: - self._im = Image.new(size=(width, height), mode="RGB", color="lightgray") + if width != None and height != None: + self._im = Image.new(size=(width, height), mode="RGB") self._draw = ImageDraw.Draw(self._im) + self.width = self._im.width + self.height = self._im.height def load_image(self, ipath): - """Loads image from disk.""" - self._im = Image.open(ipath) + self._im = Image.open(ipath).transpose(Image.FLIP_TOP_BOTTOM) + self._width = self._im.width + self._height = self._im.height - def set(self, x, y, color): # pylint: disable=invalid-name + def set(self, x, y, color): """Draw a point onto the image.""" - self._draw.point((x, self.get_height() - y - 1), fill = color) + self._draw.point((x,y), fill=color) - def get(self, x, y): # pylint: disable=invalid-name + def get(self, x, y): """Read color of image.""" # Read pixel color. - return self._im.getpixel((x, self.get_height() - y - 1)) + return self._im.getpixel((x,y)) def save_to_disk(self, fname): """Save your image to a given filename.""" - self._im.save(fname) - + imc = self._im.copy() + imc.transpose(Image.FLIP_TOP_BOTTOM).save(fname) + def get_width(self): - """Get width of image.""" return self._im.width def get_height(self): - """Get height of image.""" - return self._im.height + return self._im.height \ No newline at end of file diff --git a/tiny_shaders.py b/tiny_shaders.py index a3ca915..2513246 100644 --- a/tiny_shaders.py +++ b/tiny_shaders.py @@ -4,87 +4,265 @@ import math import our_gl as gl from geom import Matrix4D, Matrix3D, MatrixUV, \ - Vector4DType, Vector3D, Barycentric, PointUV, \ + Vector4DType, Vector3D, Barycentric, Point2D, PointUV, \ transform_3D4D3D, transform_vertex_to_screen, \ - comp_min + cross_product, matmul, transpose, comp_min -from model import ModelStorage, NormalMapType +from model import Model_Storage, NormalMapType -class DepthShader(gl.Shader): - """Shader used to save shadow buffer.""" - mdl: ModelStorage +class FlatShader(gl.Shader): + """Shader which creates a paperfold effect.""" + mdl: Model_Storage + varying_vertex = [Vector3D(0,0,0)] * 3 # Written by vertex shader, read by fragment shader + light_dir: Vector3D + M: Matrix4D + n: Vector3D - # Vertices are stored col-wise - varying_vert = Matrix3D(9*[0]) + def __init__(self, mdl, light_dir, M): + self.mdl = mdl + self.light_dir = light_dir + self.M = M # pylint: disable=invalid-name + + def vertex(self, face_idx: int, vert_idx: int): + vertex = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex + self.varying_vertex[vert_idx] = vertex + + # Get normal vector of triangle; should only be done at last vertex + if vert_idx == 2: + self.n = cross_product(self.varying_vertex[2] - self.varying_vertex[0], # pylint: disable=invalid-name + self.varying_vertex[0] - self.varying_vertex[1]).normalize() + else: + self.n = None + + return transform_vertex_to_screen(vertex, self.M) # Transform it to screen coordinates + + def fragment(self, barycentric: tuple): + if self.n is None: + return(True, None) # discard pixel + else: + cos_phi = self.n.tr() * self.light_dir + cos_phi = 0 if cos_phi < 0 else cos_phi + + color = (Vector3D(255, 255, 255) * cos_phi) // 1 + return (False, color) # Do not discard pixel and return color + +class GouraudShader(gl.Shader): + """Shader which interpolates normals of triangle vertices.""" + mdl: Model_Storage + varying_intensity = [None] * 3 # Written by vertex shader, read by fragment shader + light_dir: Vector3D + M: Matrix4D + + def __init__(self, mdl, light_dir, M): + self.mdl = mdl + self.light_dir = light_dir + self.M = M # pylint: disable=invalid-name + + def vertex(self, face_idx: int, vert_idx: int): + n = self.mdl.get_normal(face_idx, vert_idx) # pylint: disable=invalid-name + + # Get diffuse lighting intensity + self.varying_intensity[vert_idx] = max(0, n.tr() * self.light_dir) + + # Read the vertex data and return + vertex = self.mdl.get_vertex(face_idx, vert_idx) + + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.M) + + def fragment(self, barycentric: tuple): + intensity = self.varying_intensity[0]*barycentric[0] \ + + self.varying_intensity[1]*barycentric[1] \ + + self.varying_intensity[2]*barycentric[2] + + # Interpolate intensity for the current pixel + color = (Vector3D(255, 255, 255) * intensity) // 1 - uniform_M_sb: Matrix4D + # Do not discard pixel and return color + return (False, color) - def __init__(self, mdl, M_sb, depth_res): +class GouraudShaderSegregated(gl.Shader): + """Gouraud shader with distinct, segregated grey tones.""" + + mdl: Model_Storage + varying_intensity = [None] * 3 # Written by vertex shader, read by fragment shader + light_dir: Vector3D + M: Matrix4D + segregate_count = 1 + + def __init__(self, mdl, light_dir, M, segregate_count): self.mdl = mdl - self.uniform_M_sb = M_sb # pylint: disable=invalid-name - self.uniform_depth_res = depth_res + self.light_dir = light_dir + self.M = M # pylint: disable=invalid-name + self.segregate_count = segregate_count def vertex(self, face_idx: int, vert_idx: int): - vert = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex - vert = transform_vertex_to_screen(vert, self.uniform_M_sb) - self.varying_vert = self.varying_vert.set_col(vert_idx, vert) - return vert + n_tri = self.mdl.get_normal(face_idx, vert_idx) - def fragment(self, bary: Barycentric): - v_bary = Vector3D(self.varying_vert * bary) - color = (Vector3D(255, 255, 255) * v_bary.z / self.uniform_depth_res) // 1 - return (False, color) # Do not discard pixel and return color + # Get diffuse lighting intensity + self.varying_intensity[vert_idx] = max(0, n_tri.tr() * self.light_dir) + vertex = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex -class TinyShader(gl.Shader): - """A tiny shader with all techniques applied.""" - mdl: ModelStorage + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.M) + def fragment(self, barycentric: tuple): + # Interpolate intensity for current pixel + intensity = self.varying_intensity[0]*barycentric[0] \ + + self.varying_intensity[1]*barycentric[1] \ + + self.varying_intensity[2]*barycentric[2] + + # Segregates intensity values to n = 'segregate_count' distinct values + intensity = round(intensity * self.segregate_count) / self.segregate_count + + color = (Vector3D(255, 255, 255) * intensity) // 1 + + # Do not discard pixel and return color + return (False, color) + +class DiffuseGouraudShader(gl.Shader): + """Shader which combines Gouraud shading and diffuse texture color.""" + mdl: Model_Storage + + # Written by vertex shader, read by fragment shader: varying_data + varying_intensity = [None] * 3 # Points in varying_uv are stacked row-wise, 3 rows x 2 columns - varying_uv = MatrixUV(6*[0]) - varying_vert = Matrix3D(9*[0]) + varying_uv = [Point2D(0,0)] * 3 + varying_uv_shape = (3,2) + + light_dir: Vector3D + M: Matrix4D + + def __init__(self, mdl, light_dir, M): + self.mdl = mdl + self.light_dir = light_dir + self.M = M # pylint: disable=invalid-name + + def vertex(self, face_idx: int, vert_idx: int): + n_tri = self.mdl.get_normal(face_idx, vert_idx) + + # Get diffuse lighting intensity + self.varying_intensity[vert_idx] = max(0, n_tri.tr() * self.light_dir) + + # Read the vertex + vertex = self.mdl.get_vertex(face_idx, vert_idx) + + # Get uv map point for diffuse color interpolation + self.varying_uv[vert_idx] = self.mdl.get_uv_map_point(face_idx, vert_idx) + + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.M) + + def fragment(self, barycentric: tuple): + # Interpolate intensity for the current pixel + intensity = self.varying_intensity[0]*barycentric[0] \ + + self.varying_intensity[1]*barycentric[1] \ + + self.varying_intensity[2]*barycentric[2] + + # For interpolation with barycentric coordinates we need a 2 rows x 3 columns matrix + transposed_uv, tr_uv_shape = transpose(self.varying_uv, self.varying_uv_shape) + p_uv, _ = matmul(transposed_uv, tr_uv_shape, barycentric, (3,1)) + + p_uv = Point2D(*p_uv) + + color = self.mdl.get_diffuse_color(p_uv.x, p_uv.y) + color = (color * intensity) // 1 + + # Do not discard pixel and return color + return (False, color) + +class GlobalNormalmapShader(gl.Shader): + """Shader reading a global space normal map to increase detail.""" + mdl: Model_Storage + + # Points in varying_uv are stacked row-wise, 3 rows x 2 columns + varying_uv = [Point2D(0,0)] * 3 uniform_light_dir: Vector3D uniform_M_pe: Matrix4D uniform_M_sc: Matrix4D uniform_M_pe_IT: Matrix4D - uniform_M_sb_inv: Matrix4D - def __init__(self, mdl, light_dir, M_pe, M_sc, M_pe_IT, M_sb, shadow_buffer): + def __init__(self, mdl, light_dir, M_pe, M_sc, M_pe_IT): self.mdl = mdl if self.mdl.normal_map_type != NormalMapType.GLOBAL: - raise ValueError("Only use global space normalmaps with this shader") + raise ValueError self.uniform_light_dir = light_dir self.uniform_M_pe = M_pe # pylint: disable=invalid-name self.uniform_M_sc = M_sc # pylint: disable=invalid-name self.uniform_M_pe_IT = M_pe_IT # pylint: disable=invalid-name - self.uniform_M_sb = M_sb # pylint: disable=invalid-name - self.shadow_buffer = shadow_buffer def vertex(self, face_idx: int, vert_idx: int): # Read the vertex - vert = self.mdl.get_vertex(face_idx, vert_idx) - self.varying_vert = self.varying_vert.set_col(vert_idx, vert) + vertex = self.mdl.get_vertex(face_idx, vert_idx) - # Get uv map point for diffuse color interpolation and store it - self.varying_uv = \ - self.varying_uv.set_col(vert_idx, self.mdl.get_uv_map_point(face_idx, vert_idx)) + # Get uv map point for diffuse color interpolation + self.varying_uv[vert_idx] = self.mdl.get_uv_map_point(face_idx, vert_idx) - # Transform it to screen coordinates - return transform_vertex_to_screen(vert, self.uniform_M_sc) + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.uniform_M_sc) + + def fragment(self, barycentric: tuple): + # For interpolation with barycentric coordinates we need a 2 rows x 3 columns matrix + transposed_uv, tr_uv_shape = transpose(self.varying_uv, (3,2)) + p_uv, _ = matmul(transposed_uv, tr_uv_shape, barycentric, (3,1)) + + p_uv = Point2D(*p_uv) + + n_global = self.mdl.get_normal_from_map(p_uv.x, p_uv.y) + n_local = transform_3D4D3D(n_global, Vector4DType.DIRECTION, \ + self.uniform_M_pe_IT).normalize() + l_local = transform_3D4D3D(self.uniform_light_dir, Vector4DType.DIRECTION, + self.uniform_M_pe).normalize() + + # Get diffuse lighting intensity + cos_phi = max(0, n_local.tr() * l_local) - def fragment(self, bary: Barycentric): - # For interpolation with bary coordinates we need a 2 rows x 3 columns matrix - p_uv = PointUV(self.varying_uv * bary) + color = self.mdl.get_diffuse_color(p_uv.x, p_uv.y) + color = (color * cos_phi) // 1 - # Backproject interpolated point to get shadow buffer coordinates - v_bary = Vector3D(self.varying_vert * bary) - p_shadow = transform_vertex_to_screen(v_bary, self.uniform_M_sb) - z_lit = self.shadow_buffer[p_shadow.x][p_shadow.y] + # Do not discard pixel and return color + return (False, color) + +class SpecularmapShader(gl.Shader): + """Shader combining global normal map shading and specular lighting.""" + mdl: Model_Storage + + # Points in varying_uv are stacked row-wise, 3 rows x 2 columns + varying_uv = [Point2D(0,0)] * 3 + + uniform_light_dir: Vector3D + uniform_M_pe: Matrix4D + uniform_M_sc: Matrix4D + uniform_M_pe_IT: Matrix4D + + def __init__(self, mdl, light_dir, M_pe, M_sc, M_pe_IT): + self.mdl = mdl + if self.mdl.normal_map_type != NormalMapType.GLOBAL: + raise ValueError + + self.uniform_light_dir = light_dir + self.uniform_M_pe = M_pe # pylint: disable=invalid-name + self.uniform_M_sc = M_sc # pylint: disable=invalid-name + self.uniform_M_pe_IT = M_pe_IT # pylint: disable=invalid-name - shadowed_intensity = .7 if p_shadow.z + .02 * 255 < z_lit else 1.0 + def vertex(self, face_idx: int, vert_idx: int): + # Read the vertex + vertex = self.mdl.get_vertex(face_idx, vert_idx) - n_global = self.mdl.get_normal_from_map(p_uv) + # Get uv map point for diffuse color interpolation + self.varying_uv[vert_idx] = self.mdl.get_uv_map_point(face_idx, vert_idx) + + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.uniform_M_sc) + + def fragment(self, barycentric: tuple): + # For interpolation with barycentric coordinates we need a 2 rows x 3 columns matrix + transposed_uv, tr_uv_shape = transpose(self.varying_uv, (3,2)) + p_uv, _ = matmul(transposed_uv, tr_uv_shape, barycentric, (3,1)) + + n_global = self.mdl.get_normal_from_map(*p_uv) n_local = transform_3D4D3D(n_global, Vector4DType.DIRECTION, \ self.uniform_M_pe_IT).normalize() @@ -98,16 +276,107 @@ def fragment(self, bary: Barycentric): # Reflected light direction (already transformed as n and l got transformed) reflect = (2 * (cos_phi) * n_local - l_local).normalize() cos_r_z = max(0, reflect.z) # equals: reflect.tr() * Vector3D(0, 0, 1) == reflect.z - specular_intensity = math.pow(cos_r_z, self.mdl.get_specular_power_from_map(p_uv)) + specular_intensity = math.pow(cos_r_z, self.mdl.get_specular_power_from_map(*p_uv)) - # Get diffuse color and apply ambient occlusion intensity - ao_intensity = self.mdl.get_ao_intensity_from_map(p_uv) - color = self.mdl.get_diffuse_color(p_uv) + color = self.mdl.get_diffuse_color(*p_uv) # Combine base, diffuse and specular intensity - color = 10 * Vector3D(1,1,1) + \ - color * shadowed_intensity * (2.8 * (.7 * diffuse_intensity + .3 * ao_intensity) + .6 * specular_intensity) + color = 10 * Vector3D(1, 1, 1) + (diffuse_intensity + 0.5 * specular_intensity) * color color = comp_min(Vector3D(255, 255, 255), color) // 1 # Do not discard pixel and return color return (False, color) + +class TangentNormalmapShader(gl.Shader): + """Shader equal to GlobalNormalmapShader but with tangent space normal map.""" + mdl: Model_Storage + + # Points in varying_uv are stacked row-wise, 3 rows x 2 columns + varying_uv = MatrixUV(6*[0]) + + # Contains precalculated info of varying_vert to save ops in fragment shader + varying_vert = Matrix3D(9*[0]) + varying_A = Matrix3D(9*[0]) + + varying_normal = Matrix3D(9*[0]) + varying_b_u: Vector3D + varying_b_v: Vector3D + + uniform_l_local: Vector3D + uniform_M_pe: Matrix4D + uniform_M_sc: Matrix4D + uniform_M_pe_IT: Matrix4D + + def __init__(self, mdl, light_dir, M_pe, M_pe_IT, M_viewport): + self.mdl = mdl + if self.mdl.normal_map_type != NormalMapType.TANGENT: + raise ValueError("Only use tangent space normalmaps with this shader") + + # Transform light vector + l_local = transform_3D4D3D(light_dir, Vector4DType.DIRECTION, M_pe) + self.l_local = l_local.normalize() + + self.uniform_M_pe = M_pe # pylint: disable=invalid-name + self.uniform_M_pe_IT = M_pe_IT # pylint: disable=invalid-name + self.uniform_M_viewport = M_viewport # pylint: disable=invalid-name + + def vertex(self, face_idx: int, vert_idx: int): + # Store triangle vertex (after being transformed to perspective) + vertex = self.mdl.get_vertex(face_idx, vert_idx) + vertex = transform_3D4D3D(vertex, Vector4DType.POINT, self.uniform_M_pe) + self.varying_vert = self.varying_vert.set_col(vert_idx, vertex) + + n_tri_global = self.mdl.get_normal(face_idx, vert_idx) # Read normal of the vertex + n_tri_local = transform_3D4D3D(n_tri_global, Vector4DType.DIRECTION, \ + self.uniform_M_pe_IT).normalize() + + # Store triangle vertex normal (after being transformed to perspective) + self.varying_normal = self.varying_normal.set_col(vert_idx, n_tri_local) + + # Get uv map point for diffuse color interpolation and store it + self.varying_uv = \ + self.varying_uv.set_col(vert_idx, self.mdl.get_uv_map_point(face_idx, vert_idx)) + + if vert_idx == 2: + self.varying_b_u = Vector3D(self.varying_uv.u2 - self.varying_uv.u0, \ + self.varying_uv.u1 - self.varying_uv.u0, \ + 0) + + self.varying_b_v = Vector3D(self.varying_uv.v2 - self.varying_uv.v0, \ + self.varying_uv.v1 - self.varying_uv.v0, \ + 0) + + vd_0 = self.varying_vert.get_col(2) - self.varying_vert.get_col(0) + vd_1 = self.varying_vert.get_col(1) - self.varying_vert.get_col(0) + self.varying_A = (self.varying_A.set_row(0, vd_0)).set_row(1, vd_1) # pylint: disable=invalid-name + + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.uniform_M_viewport) + + def fragment(self, barycentric: tuple): + bary = Barycentric(barycentric) + p_uv = self.varying_uv * bary + p_uv = PointUV(*p_uv) + + n_bary = Vector3D(self.varying_normal * bary).normalize() + + A_inv = self.varying_A.set_row(2, n_bary).inv() # pylint: disable=invalid-name + + vect_i = (A_inv * self.varying_b_u).normalize() + vect_j = (A_inv * self.varying_b_v).normalize() + + B = Matrix3D([vect_i, # pylint: disable=invalid-name + vect_j, + n_bary]).tr() + + # Load normal of tangent space and transform to get global normal + n_local = (B * self.mdl.get_normal_from_map(*p_uv)).normalize() + + # Get diffuse lighting intensity + cos_phi = max(0, n_local.tr() * self.l_local) + + color = Vector3D(255, 255, 255)#self.mdl.get_diffuse_color(*p_uv) + color = cos_phi * color // 1 + + # Do not discard pixel and return color + return (False, color)