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..a609b87 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):
@@ -49,7 +47,7 @@ def __init__(self, *args, shape: tuple = None): # pylint: disable=unused-argumen
def __add__(self, other):
if type(self) == type(other):
- (elems, _) = mat_add(self.get_field_values(), self._shape,
+ (elems, _) = matadd(self.get_field_values(), self._shape,
other.get_field_values(), other._shape)
return type(self)(*elems)
else:
@@ -57,7 +55,7 @@ def __add__(self, other):
def __sub__(self, other):
if type(self) == type(other):
- (elems, _) = mat_sub(self.get_field_values(), self._shape,
+ (elems, _) = matsub(self.get_field_values(), self._shape,
other.get_field_values(), other._shape)
return type(self)(*elems)
@@ -65,7 +63,7 @@ def __sub__(self, other):
def __mul__(self, other):
if isinstance(other, (float, int)):
- (elems, _) = comp_mul(self.get_field_values(), self._shape, other)
+ (elems, _) = compmul(self.get_field_values(), self._shape, other)
return type(self)(*elems)
# All other cases should already have been handled in instance classes
@@ -73,14 +71,14 @@ def __mul__(self, other):
def __rmul__(self, other):
if isinstance(other, (float, int)):
- (elems, _) = comp_mul(self.get_field_values(), self._shape, other)
+ (elems, _) = compmul(self.get_field_values(), self._shape, other)
return type(self)(*elems)
raise TypeError
def __truediv__(self, other):
if isinstance(other, (float, int)):
- (elems, _) = comp_div(self.get_field_values(), self._shape, other)
+ (elems, _) = compdiv(self.get_field_values(), self._shape, other)
return type(self)(*elems)
raise TypeError
@@ -162,13 +160,11 @@ def tr(self): # pylint: disable=invalid-name
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 isinstance(self, MixinVector) and isinstance(other, MixinVector) 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__
@@ -176,7 +172,7 @@ def __mul__(self, other):
def __floordiv__(self, other):
if isinstance(other, (float, int)):
- (elems, _) = comp_floor(self.get_field_values(), self._shape, other)
+ (elems, _) = compfloor(self.get_field_values(), self._shape, other)
return type(self)(elems, shape = self._shape)
return ValueError
@@ -189,10 +185,6 @@ def tr(self): # pylint: disable=invalid-name
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())
-
class Point2D(MixinVector, metaclass=NamedTupleMetaEx):
"""Two-dimensional point with x and y ordinate."""
_shape = (2,1)
@@ -211,12 +203,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)
@@ -241,6 +227,10 @@ def expand_4D(self, vtype): # pylint: disable=invalid-name
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()
@@ -441,7 +431,7 @@ def unpack_nested_iterable_to_list(it_er: Iterable):
it_er = list(chain.from_iterable(it_er))
return it_er
-def comp_mul(mat_0: list, shape_0: tuple, factor: float):
+def compmul(mat_0: list, shape_0: tuple, factor: float):
"""Performing componentwise multiplication with factor c."""
(rows_0, cols_0) = shape_0
@@ -452,7 +442,7 @@ def comp_mul(mat_0: list, shape_0: tuple, factor: float):
# Return coefficients and shape tuple
return [e * factor for e in mat_0], shape_0
-def comp_div(mat_0: list, shape_0: tuple, divisor: float):
+def compdiv(mat_0: list, shape_0: tuple, divisor: float):
"""Performing componentwise real division by divisor."""
(rows_0, cols_0) = shape_0
@@ -463,7 +453,7 @@ def comp_div(mat_0: list, shape_0: tuple, divisor: float):
# Return coefficients and shape tuple
return [e / divisor 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, divisor: float):
"""Performing componentwise floor division."""
(rows_0, cols_0) = shape_0
@@ -474,12 +464,7 @@ def comp_floor(mat_0: list, shape_0: tuple, divisor: float):
# Return coefficients and shape tuple
return [int(e // divisor) 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 mat_add(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple):
+def matadd(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 +478,10 @@ 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):
+def matsub(mat_0: list, shape_0: tuple, mat_1: list, shape_1: tuple):
"""Performing componentwise substraction."""
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)"""
diff --git a/main.py b/main.py
index 708e0f8..d51a7df 100644
--- a/main.py
+++ b/main.py
@@ -1,42 +1,75 @@
"""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
+from tiny_shaders import FlatShader, GouraudShader, GouraudShaderSegregated, \
+ DiffuseGouraudShader, GlobalNormalmapShader, SpecularmapShader, \
+ TangentNormalmapShader, DepthShader, SpecularShadowShader
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 = 0
+ if MODEL_PROP_SET == 0:
+ OBJ_FILENAME = "obj/autumn/autumn.obj"
+ DIFFUSE_FILENAME = "obj/autumn/TEX_autumn_body_color_li.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 property selection
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
-
+ if 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
+ elif VIEW_PROP_SET == 1:
+ EYE = Vector3D(2.828, 0, 2.828)
+ CENTER = Vector3D(0, 0, 0)
+ UP = Vector3D(0, 1, 0)
+ SCALE = .8
+ else:
+ EYE = Vector3D(4, 0, 0) # 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
+
# Light property
- LIGHT_DIR = Vector3D(1, 1, 1).normalize()
+ LIGHT_DIR = Vector3D(-1, 1, 1).normalize()
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)
+ specular_map_filename=SPECULAR_MAP_FILENAME)
# Define tranformation matrices
@@ -64,55 +97,53 @@
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)
+ # 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)
+
+ SHADER_PROP_SET = 6
+ 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)
+ elif SHADER_PROP_SET == 6:
+ shader = SpecularShadowShader(mdl, LIGHT_DIR, M_pe, M_sc, M_pe_IT, M_sb, shadow_buffer)
+ else:
+ shader = FlatShader(mdl, LIGHT_DIR, M_sc)
- 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
+ zbuffer = [[-float('Inf') for bx in range(w)] for y in range(h)]
- 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)
+ # Depth shader and shadow buffer
+ depth_shader = DepthShader(mdl, M_sb, DEPTH_RES)
- # 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)
+ # Iterate model faces
+ screen_coords = ScreenCoords(9*[0])
- shadow_image.save_to_disk("renders/shadow_buffer.png")
+ # Shadow buffer run
+ print("Saving shadow buffer ...")
+ 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 = depth_shader.vertex(face_idx, face_vert_idx)
+ screen_coords = screen_coords.set_col(face_vert_idx, vert)
- # Final shader run
+ # Rasterize triangle, do not use output image
+ shadow_image = gl.draw_triangle(screen_coords, depth_shader, shadow_buffer, shadow_image)
+ shadow_image.save_to_disk("renders/shadow_buffer.png")
+ # Follow-up 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)
diff --git a/model.py b/model.py
index 950ba88..b37e2ec 100644
--- a/model.py
+++ b/model.py
@@ -143,12 +143,11 @@ class ModelStorage():
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,
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)
@@ -172,11 +171,6 @@ def __init__(self, object_name: str = None, obj_filename: str = 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]
@@ -214,15 +208,6 @@ def get_specular_power_from_map(self, pnt: PointUV):
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
-
def get_vertex_count(self):
"""Returns count of model vertices."""
return len(self.vertices)
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/tiny_image.py b/tiny_image.py
index 4aba6db..0310ff0 100644
--- a/tiny_image.py
+++ b/tiny_image.py
@@ -17,7 +17,7 @@ class TinyImage:
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")
+ self._im = Image.new(size=(width, height), mode="RGB")
self._draw = ImageDraw.Draw(self._im)
def load_image(self, ipath):
diff --git a/tiny_shaders.py b/tiny_shaders.py
index a3ca915..79cb101 100644
--- a/tiny_shaders.py
+++ b/tiny_shaders.py
@@ -6,10 +6,372 @@
from geom import Matrix4D, Matrix3D, MatrixUV, \
Vector4DType, Vector3D, Barycentric, PointUV, \
transform_3D4D3D, transform_vertex_to_screen, \
- comp_min
+ cross_product, comp_min
from model import ModelStorage, NormalMapType
+class FlatShader(gl.Shader):
+ """Shader which creates a paperfold effect."""
+ mdl: ModelStorage
+
+ # Vertices are stored row-wise
+ varying_vert = Matrix3D(9*[0])
+
+ varying_n_tri: Vector3D
+ uniform_l_global: Vector3D
+ uniform_M: Matrix4D
+
+ def __init__(self, mdl, light_dir, M):
+ self.mdl = mdl
+ self.uniform_l_global = light_dir
+ self.uniform_M = M # pylint: disable=invalid-name
+
+ def vertex(self, face_idx: int, vert_idx: int):
+ vert = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex
+ self.varying_vert = self.varying_vert.set_row(vert_idx, vert)
+
+ # Get normal vector of triangle; should only be done at last vertex
+ if vert_idx == 2:
+ v_0 = Vector3D(self.varying_vert.get_row(0))
+ v_1 = Vector3D(self.varying_vert.get_row(1))
+ v_2 = Vector3D(self.varying_vert.get_row(2))
+ self.varying_n_tri = cross_product(v_2 - v_0, v_0 - v_1).normalize()
+
+ return transform_vertex_to_screen(vert, self.uniform_M) # Transform it to screen coordinates
+
+ def fragment(self, bary: Barycentric):
+ cos_phi = max(0, self.varying_n_tri.tr() * self.uniform_l_global)
+
+ 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: ModelStorage
+
+ # Written by vertex shader, read by fragment shader
+ varying_intensity = Vector3D(0, 0, 0, shape = (1,3))
+ uniform_l_global: Vector3D
+ uniform_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
+ cos_phi = max(0, n_tri.tr() * self.uniform_l_global)
+ self.varying_intensity = self.varying_intensity.set_col(vert_idx, cos_phi)
+
+ # 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.uniform_M)
+
+ def fragment(self, bary: Barycentric):
+ intensity = self.varying_intensity * bary
+
+ # Interpolate intensity for the current pixel
+ color = (Vector3D(255, 255, 255) * intensity) // 1
+
+ # Do not discard pixel and return color
+ return (False, color)
+
+class GouraudShaderSegregated(gl.Shader):
+ """Gouraud shader with distinct, segregated grey tones."""
+ mdl: ModelStorage
+ varying_intensity = Vector3D(0, 0, 0, shape = (1,3))
+ uniform_l_global: Vector3D
+ uniform_M: Matrix4D
+ segregate_count = 1
+
+ def __init__(self, mdl, light_dir, M, segregate_count):
+ self.mdl = mdl
+ self.uniform_l_global = light_dir
+ self.uniform_M = M # pylint: disable=invalid-name
+ self.segregate_count = segregate_count
+
+ def vertex(self, face_idx: int, vert_idx: int):
+ n_tri = self.mdl.get_normal(face_idx, vert_idx)
+
+ # Get diffuse lighting intensity
+ cos_phi = max(0, n_tri.tr() * self.uniform_l_global)
+ self.varying_intensity = self.varying_intensity.set_col(vert_idx, cos_phi)
+
+ vert = self.mdl.get_vertex(face_idx, vert_idx) # Read the vertex
+
+ # Transform it to screen coordinates
+ return transform_vertex_to_screen(vert, self.uniform_M)
+
+ def fragment(self, bary: Barycentric):
+ # Interpolate intensity for current pixel
+ intensity = self.varying_intensity * bary
+
+ # 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: ModelStorage
+
+ # Written by vertex shader, read by fragment shader: varying_data
+ varying_intensity = Vector3D(0, 0, 0, shape = (1,3))
+ # Points in varying_uv are stacked row-wise, 3 rows x 2 columns
+ varying_uv = MatrixUV(6*[0])
+
+ uniform_l_global: Vector3D
+ uniform_M: Matrix4D
+
+ def __init__(self, mdl, light_dir, M):
+ self.mdl = mdl
+ self.uniform_l_global = light_dir
+ self.uniform_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
+ cos_phi = max(0, n_tri.tr() * self.uniform_l_global)
+ self.varying_intensity = self.varying_intensity.set_col(vert_idx, cos_phi)
+
+ # Read the vertex
+ 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))
+
+ # Transform it to screen coordinates
+ return transform_vertex_to_screen(vertex, self.uniform_M)
+
+ def fragment(self, bary: Barycentric):
+ # Interpolate intensity for the current pixel
+ intensity = self.varying_intensity * bary
+
+ # 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)
+ 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: ModelStorage
+
+ # Points in varying_uv are stacked row-wise, 3 rows x 2 columns
+ varying_uv = MatrixUV(6*[0])
+
+ 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("Only use global space normalmaps with this shader")
+
+ 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
+
+ def vertex(self, face_idx: int, vert_idx: int):
+ # Read the vertex
+ 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))
+
+ # Transform it to screen coordinates
+ return transform_vertex_to_screen(vertex, self.uniform_M_sc)
+
+ 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_global = self.mdl.get_normal_from_map(p_uv)
+ 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)
+
+ color = self.mdl.get_diffuse_color(p_uv)
+ color = (color * cos_phi) // 1
+
+ # Do not discard pixel and return color
+ return (False, color)
+
+class SpecularmapShader(gl.Shader):
+ """Shader combining global normal map shading and specular lighting."""
+ mdl: ModelStorage
+
+ # Points in varying_uv are stacked row-wise, 3 rows x 2 columns
+ varying_uv = MatrixUV(6*[0])
+
+ 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("Only use global space normalmaps with this shader")
+
+ 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
+
+ def vertex(self, face_idx: int, vert_idx: int):
+ # Read the vertex
+ 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))
+
+ # Transform it to screen coordinates
+ return transform_vertex_to_screen(vertex, self.uniform_M_sc)
+
+ 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_global = self.mdl.get_normal_from_map(p_uv)
+ 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()
+ cos_phi = n_local.tr() * l_local
+
+ # Get diffuse lighting intensity
+ diffuse_intensity = max(0, cos_phi)
+
+ # 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))
+
+ color = self.mdl.get_diffuse_color(p_uv)
+
+ # Combine base, diffuse and 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: ModelStorage
+
+ # 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 only tangent space normalmaps with this shader")
+
+ # Transform light vector
+ l_local = transform_3D4D3D(light_dir, Vector4DType.DIRECTION, M_pe)
+ self.uniform_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)
+ vert = self.mdl.get_vertex(face_idx, vert_idx)
+ vert = transform_3D4D3D(vert, Vector4DType.POINT, self.uniform_M_pe)
+ self.varying_vert = self.varying_vert.set_col(vert_idx, vert)
+
+ 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.u_2 - self.varying_uv.u_0, \
+ self.varying_uv.u_1 - self.varying_uv.u_0, \
+ 0)
+
+ self.varying_b_v = Vector3D(self.varying_uv.v_2 - self.varying_uv.v_0, \
+ self.varying_uv.v_1 - self.varying_uv.v_0, \
+ 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(vert, self.uniform_M_viewport)
+
+ def fragment(self, bary: Barycentric):
+ p_uv = PointUV(self.varying_uv * bary)
+
+ n_bary = (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.uniform_l_local)
+
+ color = self.mdl.get_diffuse_color(p_uv)
+ color = cos_phi * color // 1
+
+ # Do not discard pixel and return color
+ return (False, color)
+
class DepthShader(gl.Shader):
"""Shader used to save shadow buffer."""
mdl: ModelStorage
@@ -35,8 +397,8 @@ def fragment(self, bary: Barycentric):
color = (Vector3D(255, 255, 255) * v_bary.z / self.uniform_depth_res) // 1
return (False, color) # Do not discard pixel and return color
-class TinyShader(gl.Shader):
- """A tiny shader with all techniques applied."""
+class SpecularShadowShader(gl.Shader):
+ """Shader combining global normal map shading, specular lighting and shadows."""
mdl: ModelStorage
# Points in varying_uv are stacked row-wise, 3 rows x 2 columns
@@ -65,7 +427,7 @@ 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)
-
+
# 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))
@@ -82,7 +444,7 @@ def fragment(self, bary: Barycentric):
p_shadow = transform_vertex_to_screen(v_bary, self.uniform_M_sb)
z_lit = self.shadow_buffer[p_shadow.x][p_shadow.y]
- shadowed_intensity = .7 if p_shadow.z + .02 * 255 < z_lit else 1.0
+ shadowed_intensity = .3 if p_shadow.z + .02 * 255 < z_lit else 1.0
n_global = self.mdl.get_normal_from_map(p_uv)
n_local = transform_3D4D3D(n_global, Vector4DType.DIRECTION, \
@@ -99,15 +461,13 @@ def fragment(self, bary: Barycentric):
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))
+
- # 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)
# 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 = 20 * Vector3D(1,1,1) + color * shadowed_intensity * (1.2 * diffuse_intensity + .6 * specular_intensity)
color = comp_min(Vector3D(255, 255, 255), color) // 1
# Do not discard pixel and return color
- return (False, color)
+ return (False, color)
\ No newline at end of file