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 wire mesh
-
-
-### Rendered 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)