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..c121c1d 100644 --- a/geom.py +++ b/geom.py @@ -1,253 +1,210 @@ """The geom module: Includes matrix and vector classes based on NamedTuple and basic algebra.""" -from enum import Enum - +from typing import NamedTuple, NamedTupleMeta import typing +from collections import namedtuple from collections.abc import Iterable - +import numpy as np +import math from itertools import chain -from functools import reduce import operator +from enum import Enum -from math import sqrt - -import numpy as np - -class Vector4DType(Enum): - """Enum specifying if 4D vector is meant to be direction or point.""" +class Vector_4D_Type(Enum): DIRECTION = 0 POINT = 1 class NamedTupleMetaEx(typing.NamedTupleMeta): - """typing.NamedTuple metaclass to provide mixin functionalty alongside typing.NamedTuples.""" - def __new__(cls, typename, bases, ns): - cls_obj = super().__new__(cls, typename+'_nm_base', bases, ns) + def __new__(self, typename, bases, ns): + cls_obj = super().__new__(self, typename+'_nm_base', bases, ns) bases = bases + (cls_obj,) return type(typename, bases, {}) -class MixinAlgebra(): - """Mixin providing basic functionality for matrices and vectors based on typing.NamedTuple.""" - def __new__(cls, *args, shape: tuple = None): # pylint: disable=unused-argument +class MixinAlgebra(): + def __new__(self, *args, shape: tuple = None): if isinstance(args[0], Iterable): - if len(cls._fields) > 1: - return super().__new__(cls, *unpack_nested_iterable_to_list(args[0])) + if len(self._fields) > 1: + return super().__new__(self, *unpack_nested_iterable_to_list(args[0])) else: - return super().__new__(cls, unpack_nested_iterable_to_list(args)) + return super().__new__(self, unpack_nested_iterable_to_list(args)) else: - if len(cls._fields) > 1: - return super().__new__(cls, *args) + if len(self._fields) > 1: + return super().__new__(self, *args) else: - return super().__new__(cls, list(args)) - + return super().__new__(self, list(args)) + # Overwrite __init__ to add 'shape' keyword parameter - def __init__(self, *args, shape: tuple = None): # pylint: disable=unused-argument + def __init__(self, *args, shape: tuple = None): if not shape is None: self._shape = shape - + if len(self.get_field_values()) != self._shape[0] * self._shape[1]: 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) - else: - raise TypeError - + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) + 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) 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) + 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) + 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) def get_field_values(self): - """Returns all field values of the typing.NamedTuple._asdict method as list. - If there is solely a single list field this list will be returned.""" if len(self._fields) == 1 and 'elems' in self.__annotations__: return list(self._asdict().values())[0] else: return list(self._asdict().values()) - + def __str__(self): prefix = self.__class__.__name__ + "(" with np.printoptions(precision = 3, suppress = True): npa = np.array(self).reshape(self._shape) return prefix + np.array2string(npa, prefix=prefix) + ")" - 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)) - - 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): - """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] - - if len(lst) == 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 - - 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) + if MixinVector in other.__class__.__bases__: + (elems, s) = 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 = s) + + elif MixinMatrix in other.__class__.__bases__: + (elems, s) = 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 = s) 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) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems) - def tr(self): # pylint: disable=invalid-name - """Returns transpose of a matrix.""" + def tr(self): (elems, shape) = transpose(self.get_field_values(), self._shape) - return type(self)(elems, shape = shape) + cl_type = globals()[self.__class__.__name__] + return cl_type(*elems, shape = shape) + + def set_row(self, row_idx, other: Iterable): + (r,c) = self._shape + li = unpack_nested_iterable_to_list(other) + + if len(other) == c and row_idx < r: + elems = self.get_field_values() + start_idx = row_idx * (r - 1) + elems[start_idx : start_idx+len(li)] = 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): + return self.tr().set_row(col_idx, other).tr() 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) + def __mul__(self, other): + 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 - """Returns a transposed vector.""" + def tr(self): # Transpose MixinVector - (rows, cols) = self._shape - + (s,h) = self._shape + + cl_type = globals()[self.__class__.__name__] elems = self._asdict().values() - return type(self)(elems, shape = (cols, rows)) + return cl_type(*elems, shape = (h,s)) - def abs(self): - """Returns length of vector.""" - return vect_norm(self.get_field_values()) - -class Point2D(MixinVector, metaclass=NamedTupleMetaEx): - """Two-dimensional point with x and y ordinate.""" +class Point_2D(MixinVector, metaclass=NamedTupleMetaEx): _shape = (2,1) x: float y: float -class PointUV(MixinVector, metaclass=NamedTupleMetaEx): - """Two-dimensional point with u and v ordinate for interpolated texture and map coordinates.""" +class Point_UV(MixinVector, metaclass=NamedTupleMetaEx): _shape = (2,1) u: float v: float class Barycentric(MixinVector, metaclass=NamedTupleMetaEx): - """Three-dimensional vector to store barycentric coordinates of a triangle.""" _shape = (3,1) 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.""" +class Vector_3D(MixinVector, metaclass=NamedTupleMetaEx): _shape = (3,1) x: float y: float z: float - def expand_4D(self, vtype): # pylint: disable=invalid-name - """Expands 3D vector to 4D vector regarding the given type. - - Options: - Vector4D(x, y, z, 0) for vectors with directional meaning. - 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) + def expand_4D(self, vtype): + 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: - return Vector4D(self.x, self.y, self.z, 1, shape = new_shape) + if vtype == Vector_4D_Type.DIRECTION: + return Vector_4D(self.x, self.y, self.z, 0, shape = new_shape) + elif vtype == Vector_4D_Type.POINT: + return Vector_4D(self.x, self.y, self.z, 1, shape = new_shape) - return ValueError + def abs(self): + 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 + def norm(self): + ab = self.abs() + if ab > 0: + return self / ab + else: + return None -class Vector4D(MixinVector, metaclass=NamedTupleMetaEx): - """Four-dimensional vector with x, y, y and a component.""" +class Vector_4D(MixinVector, metaclass=NamedTupleMetaEx): _shape = (4,1) _space = None x: float @@ -255,47 +212,31 @@ class Vector4D(MixinVector, metaclass=NamedTupleMetaEx): z: float a: float - def project_3D(self, vtype): # pylint: disable=invalid-name - """Reduces four-dimensional vector to three dimensions. - - If vector has directional meaning the 'a' component is just omited. - If vector identifies vertex in 3D space the vertex is projected to screen plane z = 1 - by diving all components through last component 'a'. - """ - - new_shape = (3,1) if is_col_vect(self._shape) else (1,3) - - if vtype == Vector4DType.DIRECTION: - return Vector3D(self.x, self.y, self.z, shape = new_shape) - if vtype == Vector4DType.POINT: - return Vector3D(self.x / self.a, self.y / self.a, self.z / self.a, shape = new_shape) - - raise ValueError - -class MatrixNxN(MixinMatrix, metaclass=NamedTupleMetaEx): - """Matrix with any size (n x n). + def project_3D(self, vtype): + if is_col_vect(self._shape): + new_shape = (3,1) + else: + new_shape = (1,3) - Parameters: - elems: list containing all matrix components. List may have nested lists. - shape: tuple containing the matrix shape. - e.g. (2,3) for two rows and three columns - """ + if vtype == Vector_4D_Type.DIRECTION: + return Vector_3D(self.x, self.y, self.z, shape = new_shape) + elif vtype == Vector_4D_Type.POINT: + return Vector_3D(self.x / self.a, self.y / self.a, self.z / self.a, shape = new_shape) +class Matrix_NxN(MixinMatrix, metaclass=NamedTupleMetaEx): _shape = None elems: list -class MatrixUV(MixinMatrix, metaclass=NamedTupleMetaEx): - """Matrix with size (2 x 3) holding three pairs of uv coordinates.""" +class Matrix_uv(MixinMatrix, metaclass=NamedTupleMetaEx): _shape = (2,3) - u_0: float - u_1: float - u_2: float - v_0: float - v_1: float - v_2: float - -class Matrix3D(MixinMatrix, metaclass=NamedTupleMetaEx): - """Three-dimensional square matrix.""" + u0: float + u1: float + u2: float + v0: float + v1: float + v2: float + +class Matrix_3D(MixinMatrix, metaclass=NamedTupleMetaEx): _shape = (3, 3) a11: float a12: float @@ -307,23 +248,7 @@ 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.""" +class Matrix_4D(MixinMatrix, metaclass=NamedTupleMetaEx): _shape = (4, 4) a11: float a12: float @@ -343,22 +268,15 @@ class Matrix4D(MixinMatrix, metaclass=NamedTupleMetaEx): a44: float def matmul(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): - """Function performing matrix multiplication. - - Parameters: - mat_0: list containg components of first matrix - shape_0: tuple containing size of first matrix - mat_1: list containg components of second matrix - shape_1: tuple containing size of second matrix - """ - + (rows_0, cols_0) = shape_0 (rows_1, cols_1) = shape_1 if len(mat_0) != (rows_0 * cols_0) or \ len(mat_1) != (rows_1 * cols_1) or \ cols_0 != rows_1: - raise ShapeMissmatchException + # Indices to not match to perform matrix multiplication + raise(ShapeMissmatchException) else: # Example: (3,4) * (4,6) -> will give 3 x 6; cols_0 rows_1 must match # Init coefficients x = rows(mat_0) * cols(mat_1) @@ -366,19 +284,18 @@ def matmul(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): elems = [None for i in range(rows_0 * cols_1)] for row in range(rows_0): for col in range(cols_1): - comp_sum = 0 - for ele in range(cols_0): + su = 0 + for it in range(cols_0): # Actually cols_0 and rows_1 are and must be the same - c_0 = mat_0[row * cols_0 + ele] - c_1 = mat_1[ele * cols_1 + col] - comp_sum += c_0 * c_1 - elems[row * cols_1 + col] = comp_sum + c_0 = mat_0[row * cols_0 + it] + c_1 = mat_1[it * cols_1 + col] + su += c_0 * c_1 + elems[row * cols_1 + col] = su # Return coefficients and shape tuple return elems, (rows_0, cols_1) def transpose(mat: list, shape: tuple): - """Function performing matrix transpose.""" (rows, cols) = shape @@ -390,97 +307,78 @@ def transpose(mat: list, shape: tuple): elems = [None for i in range(rows * cols)] for row in range(rows): for col in range(cols): - ele = mat[row * cols + col] # Read row-wise - elems[col * rows + row] = ele - + e = mat[row * cols + col] # Read row-wise + elems[col * rows + row] = e + return elems, (cols, rows) def inverse(mat: list, shape:tuple): - """Calculate inverse of a matrix using numpy.""" - arr_inv = np.linalg.inv(np.reshape(mat, shape)) - return arr_inv.flatten().tolist(), shape - -def cross_product(v_0: Vector3D, v_1: Vector3D): - """Calculates cross product of two three-dimensional vectors.""" - c_0 = v_0.y * v_1.z - v_0.z * v_1.y - c_1 = v_0.z * v_1.x - v_0.x * v_1.z - 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 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. - """ - - v = transform_3D4D3D(v, Vector4DType.POINT, M) - v_z = v.z + mr = np.linalg.inv(np.reshape(mat, shape)) + return mr.flatten().tolist(), shape + +def cross_product(v0: Vector_3D, v1: Vector_3D): + c0 = v0.y*v1.z - v0.z*v1.y + c1 = v0.z*v1.x - v0.x*v1.z + c2 = v0.x*v1.y - v0.y*v1.x + return Vector_3D(c0, c1, c2) + +def comp_min(v0, v1): + return Vector_3D(min(v0.x, v1.x), min(v0.y, v1.y), min(v0.z, v1.z)) + +def comp_max(v0, v1): + return Vector_3D(max(v0.x, v1.x), max(v0.y, v1.y), max(v0.z, v1.z)) + +def transform_vertex_to_screen(v : Vector_3D, M: Matrix_4D): + v = transform_3D4D3D(v, Vector_4D_Type.POINT, M) + vz = v.z v = v // 1 - return Vector3D(v.x, v.y, v_z) - -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 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): + return Vector_3D(v.x, v.y, vz) + +def transform_3D4D3D(v: Vector_3D, vtype: Vector_4D_Type, M: Matrix_4D): + v = M * v.expand_4D(vtype) + return v.project_3D(vtype) + +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): # Indices to not match to perform matrix substraction - raise ShapeMissmatchException + 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 compdiv(mat_0: list, shape_0: tuple, c: float): -def comp_div(mat_0: list, shape_0: tuple, divisor: float): - """Performing componentwise real division by divisor.""" (rows_0, cols_0) = shape_0 if len(mat_0) != (rows_0 * cols_0): # Indices to not match to perform matrix substraction - raise ShapeMissmatchException + 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 compfloor(mat_0: list, shape_0: tuple, c: float): -def comp_floor(mat_0: list, shape_0: tuple, divisor: float): - """Performing componentwise floor division.""" (rows_0, cols_0) = shape_0 if len(mat_0) != (rows_0 * cols_0): # Indices to not match to perform matrix substraction - raise ShapeMissmatchException + 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 @@ -488,42 +386,20 @@ def mat_add(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple): len(mat_1) != (rows_1 * cols_1) or \ shape_0 != shape_1: # Indices to not match to perform matrix substraction - raise ShapeMissmatchException + raise(ShapeMissmatchException) else: # 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)""" return shape[0] < shape[1] 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 \ No newline at end of file diff --git a/main.py b/main.py index 708e0f8..3954c29 100644 --- a/main.py +++ b/main.py @@ -1,61 +1,91 @@ -"""A tiny shader fork written in Python 3""" -import progressbar +import numpy as np +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, cross_product +from model import Model_Storage, get_model_face_ids, get_vertices, NormalMapType +from tiny_shaders import Flat_Shader, Gouraud_Shader, Gouraud_Shader_Segregated, Diffuse_Gouraud_Shader, \ + Global_Normalmap_Shader, Specularmap_Shader, Tangent_Normalmap_Shader 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 - # + # Generate model transformation matrix which transforms 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]) # Generate cam transformation - M_lookat = gl.lookat(EYE, CENTER, UP) + 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,61 +94,35 @@ 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 = Gouraud_Shader(mdl, light_dir, M_sc) + elif shader_prop_set == 1: + shader = Gouraud_Shader_Segregated(mdl, light_dir, M_sc, 4) + elif shader_prop_set == 2: + shader = Diffuse_Gouraud_Shader(mdl, light_dir, M_sc) + elif shader_prop_set == 3: + shader = Global_Normalmap_Shader(mdl, light_dir, M_pe, M_sc, M_pe_IT) + elif shader_prop_set == 4: + shader = Specularmap_Shader(mdl, light_dir, M_pe, M_sc, M_pe_IT) + elif shader_prop_set == 5: + shader = Tangent_Normalmap_Shader(mdl, light_dir, M_pe, M_pe_IT, M_viewport) + else: + shader = Flat_Shader(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) - image.save_to_disk(OUTPUT_FILENAME) + image.save_to_disk(output_filename) \ No newline at end of file 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..8acb3d2 100644 --- a/tiny_shaders.py +++ b/tiny_shaders.py @@ -1,113 +1,303 @@ -"""This modules contains all tiny shaders. Shaders all have the - same structure maintained by an abstract base class in our_gl module.""" +import our_gl as gl +from geom import Matrix_4D, Matrix_3D, Matrix_uv, Vector_3D, Barycentric, Point_2D, Point_UV, transform_vertex_to_screen, cross_product, matmul, transpose, \ + Vector_4D_Type, transform_3D4D3D, Vector_4D, comp_min, unpack_nested_iterable_to_list +from model import Model_Storage, NormalMapType import math -import our_gl as gl -from geom import Matrix4D, Matrix3D, MatrixUV, \ - Vector4DType, Vector3D, Barycentric, PointUV, \ - transform_3D4D3D, transform_vertex_to_screen, \ - comp_min -from model import ModelStorage, NormalMapType +class Flat_Shader(gl.Shader): + mdl: Model_Storage + varying_vertex = [Vector_3D(0,0,0)] * 3 # Written by vertex shader, read by fragment shader + light_dir: Vector_3D + M: Matrix_4D + n: Vector_3D + + def __init__(self, mdl, light_dir, M): + self.mdl = mdl + self.light_dir = light_dir + self.M = M + + 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], + self.varying_vertex[0] - self.varying_vertex[1]).norm() + else: + self.n = None + + return transform_vertex_to_screen(vertex, self.M) # Transform it to screen coordinates -class DepthShader(gl.Shader): - """Shader used to save shadow buffer.""" - mdl: ModelStorage + 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 - # Vertices are stored col-wise - varying_vert = Matrix3D(9*[0]) + color = (Vector_3D(255, 255, 255) * cos_phi) // 1 + return (False, color) # Do not discard pixel and return color - uniform_M_sb: Matrix4D +class Gouraud_Shader(gl.Shader): + mdl: Model_Storage + varying_intensity = [None] * 3 # Written by vertex shader, read by fragment shader + light_dir: Vector_3D + M: Matrix_4D - def __init__(self, mdl, M_sb, depth_res): + def __init__(self, mdl, light_dir, M): 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 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 - - 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 + n = self.mdl.get_normal(face_idx, vert_idx) + self.varying_intensity[vert_idx] = max(0, n.tr() * self.light_dir) # Get diffuse lighting intensity + vertex = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex + + return transform_vertex_to_screen(vertex, self.M) # Transform it to screen coordinates + + 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 = (Vector_3D(255, 255, 255) * intensity) // 1 return (False, color) # Do not discard pixel and return color -class TinyShader(gl.Shader): - """A tiny shader with all techniques applied.""" - mdl: ModelStorage +class Gouraud_Shader_Segregated(gl.Shader): + mdl: Model_Storage + varying_intensity = [None] * 3 # Written by vertex shader, read by fragment shader + light_dir: Vector_3D + M: Matrix_4D + segregate_count = 1 + + def __init__(self, mdl, light_dir, M, segregate_count): + self.mdl = mdl + self.light_dir = light_dir + self.M = M + self.segregate_count = segregate_count + + def vertex(self, face_idx: int, vert_idx: int): + n = self.mdl.get_normal(face_idx, vert_idx) + self.varying_intensity[vert_idx] = max(0, n.tr() * self.light_dir) # Get diffuse lighting intensity + vertex = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex + + return transform_vertex_to_screen(vertex, self.M) # Transform it to screen coordinates + + 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 + + # Segregates intensity values to n = 'segregate_count' distinct values + intensity = round(intensity * self.segregate_count) / self.segregate_count + + color = (Vector_3D(255, 255, 255) * intensity) // 1 + return (False, color) # Do not discard pixel and return color +class Diffuse_Gouraud_Shader(gl.Shader): + mdl: Model_Storage + varying_intensity = [None] * 3 # Written by vertex shader, read by fragment shader + # 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 = [Point_2D(0,0)] * 3 + varying_uv_shape = (3,2) - uniform_light_dir: Vector3D - uniform_M_pe: Matrix4D - uniform_M_sc: Matrix4D - uniform_M_pe_IT: Matrix4D - uniform_M_sb_inv: Matrix4D + light_dir: Vector_3D + M: Matrix_4D - def __init__(self, mdl, light_dir, M_pe, M_sc, M_pe_IT, M_sb, shadow_buffer): + def __init__(self, mdl, light_dir, M): + self.mdl = mdl + self.light_dir = light_dir + self.M = M + + def vertex(self, face_idx: int, vert_idx: int): + n = self.mdl.get_normal(face_idx, vert_idx) + self.varying_intensity[vert_idx] = max(0, n.tr() * self.light_dir) # Get diffuse lighting intensity + vertex = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex + + self.varying_uv[vert_idx] = self.mdl.get_uv_map_point(face_idx, vert_idx) # Get uv map point for diffuse color interpolation + return transform_vertex_to_screen(vertex, self.M) # Transform it to screen coordinates + + 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 + + # 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 = Point_2D(*p_uv) + + color = self.mdl.get_diffuse_color(p_uv.x, p_uv.y) + color = (color * intensity) // 1 + return (False, color) # Do not discard pixel and return color + +class Global_Normalmap_Shader(gl.Shader): + mdl: Model_Storage + + # Points in varying_uv are stacked row-wise, 3 rows x 2 columns + varying_uv = [Point_2D(0,0)] * 3 + + uniform_light_dir: Vector_3D + uniform_M_pe: Matrix_4D + uniform_M_sc: Matrix_4D + uniform_M_pe_IT: Matrix_4D + + 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 + self.uniform_M_pe = M_pe + self.uniform_M_sc = M_sc + self.uniform_M_pe_IT = M_pe_IT 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) # Read the vertex + + self.varying_uv[vert_idx] = self.mdl.get_uv_map_point(face_idx, vert_idx) # Get uv map point for diffuse color interpolation + return transform_vertex_to_screen(vertex, self.uniform_M_sc) # Transform it to screen coordinates - # 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)) + 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)) - # Transform it to screen coordinates - return transform_vertex_to_screen(vert, self.uniform_M_sc) + p_uv = Point_2D(*p_uv) - 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) + n = self.mdl.get_normal_from_map(p_uv.x, p_uv.y) + n = transform_3D4D3D(n, Vector_4D_Type.DIRECTION, self.uniform_M_pe_IT).norm() + l = transform_3D4D3D(self.uniform_light_dir, Vector_4D_Type.DIRECTION, self.uniform_M_pe).norm() + intensity = max(0, n.tr() * l) # Get diffuse lighting intensity - # 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] + color = Vector_3D(255,255,255)#self.mdl.get_diffuse_color(p_uv.x, p_uv.y) + color = (color * intensity) // 1 + return (False, color) # Do not discard pixel and return color - shadowed_intensity = .7 if p_shadow.z + .02 * 255 < z_lit else 1.0 +class Specularmap_Shader(gl.Shader): + mdl: Model_Storage + + # Points in varying_uv are stacked row-wise, 3 rows x 2 columns + varying_uv = [Point_2D(0,0)] * 3 - n_global = self.mdl.get_normal_from_map(p_uv) - n_local = transform_3D4D3D(n_global, Vector4DType.DIRECTION, \ - self.uniform_M_pe_IT).normalize() + uniform_light_dir: Vector_3D + uniform_M_pe: Matrix_4D + uniform_M_sc: Matrix_4D + uniform_M_pe_IT: Matrix_4D - l_local = transform_3D4D3D(self.uniform_light_dir, Vector4DType.DIRECTION, \ - self.uniform_M_pe).normalize() - cos_phi = n_local.tr() * l_local + 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 + self.uniform_M_sc = M_sc + self.uniform_M_pe_IT = M_pe_IT - # Get diffuse lighting intensity - diffuse_intensity = max(0, cos_phi) + def vertex(self, face_idx: int, vert_idx: int): + vertex = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex + + self.varying_uv[vert_idx] = self.mdl.get_uv_map_point(face_idx, vert_idx) # Get uv map point for diffuse color interpolation + return transform_vertex_to_screen(vertex, self.uniform_M_sc) # Transform it to screen coordinates + + 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 = self.mdl.get_normal_from_map(*p_uv) + n = transform_3D4D3D(n, Vector_4D_Type.DIRECTION, self.uniform_M_pe_IT).norm() + l = transform_3D4D3D(self.uniform_light_dir, Vector_4D_Type.DIRECTION, self.uniform_M_pe).norm() + n_l = n.tr() * l + diffuse_intensity = max(0, n_l) # Get diffuse lighting intensity # 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)) + r = (2 * (n_l) * n - l).norm() + cos_r_z = max(0, r.z) # r.tr() * Vector_3D(0, 0, 1) == r.z + 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 = comp_min(Vector3D(255, 255, 255), color) // 1 + color = 10 * Vector_3D(1, 1, 1) + (diffuse_intensity + 0.5 * specular_intensity) * color + color = comp_min(Vector_3D(255, 255, 255), color) // 1 + return (False, color) # Do not discard pixel and return color + +class Tangent_Normalmap_Shader(gl.Shader): + mdl: Model_Storage + + # Points in varying_uv are stacked row-wise, 3 rows x 2 columns + varying_uv = Matrix_uv([0]*6) + varying_vert = [Vector_3D(0,0,0)] * 3 + varying_nvert = [Vector_3D(0,0,0)] * 3 + + uniform_light_dir: Vector_3D + uniform_M_pe: Matrix_4D + uniform_M_sc: Matrix_4D + uniform_M_pe_IT: Matrix_4D + + 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") + + self.uniform_light_dir = light_dir + self.uniform_M_pe = M_pe + self.uniform_M_pe_IT = M_pe_IT + self.uniform_M_viewport = M_viewport - # Do not discard pixel and return color - return (False, color) + 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, Vector_4D_Type.POINT, self.uniform_M_pe) + self.varying_vert[vert_idx] = vertex + + # Store triangle vertex normal (after being transformed to perspective) + nvert = self.mdl.get_normal(face_idx, vert_idx) # Read normal of the vertex + nvert = transform_3D4D3D(nvert, Vector_4D_Type.DIRECTION, self.uniform_M_pe_IT).norm() + self.varying_nvert[vert_idx] = nvert # Already projected onto screen plane + + # 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)) + + # Transform it to screen coordinates + return transform_vertex_to_screen(vertex, self.uniform_M_viewport) + + def fragment(self, barycentric: tuple): + bary = Barycentric(barycentric) + p = self.varying_uv * bary + p_uv = Point_UV(*p) + + transposed_nvert, tr_nvert_shape = transpose(unpack_nested_iterable_to_list(self.varying_nvert), (3,3)) + n_bary, _ = matmul(transposed_nvert, tr_nvert_shape, barycentric, (3,1)) + n_bary = Vector_3D(*n_bary).norm() + + A_inv = Matrix_3D([self.varying_vert[2] - self.varying_vert[0], + self.varying_vert[1] - self.varying_vert[0], + n_bary ]).inv() + + b_u = Vector_3D(self.varying_uv.u2 - self.varying_uv.u0, self.varying_uv.u1 - self.varying_uv.u0, 0) + b_v = Vector_3D(self.varying_uv.v2 - self.varying_uv.v0, self.varying_uv.v1 - self.varying_uv.v0, 0) + + i = (A_inv * b_u).norm() + j = (A_inv * b_v).norm() + + B = Matrix_3D([i , + j , + n_bary]).tr() + + n = (B * self.mdl.get_normal_from_map(*p_uv)).norm() # Load normal of tangent space and multiply + + l = transform_3D4D3D(self.uniform_light_dir, Vector_4D_Type.DIRECTION, self.uniform_M_pe).norm() + n_l = n.tr() * l + diffuse_intensity = max(0, n_l) # Get diffuse lighting intensity + + color = Vector_3D(255,255,255)#self.mdl.get_diffuse_color(*p_uv) + color = diffuse_intensity * color // 1 + + return (False, color) # Do not discard pixel and return color