diff --git a/README.md b/README.md index 5384a69..0ffbd16 100644 --- a/README.md +++ b/README.md @@ -5,29 +5,17 @@ Make sure to have python3 running and the following libs are installed: `pip install pillow` -## What is special about this fork? +Blender generate .obj-file steps: -For the general idea please visit [https://github.com/ssloy/tinyrenderer](https://github.com/ssloy/tinyrenderer) +1. object mode -> Object -> Convert to -> Mesh from Curve/Meta/Surf/Text +1. remove all keyframes +1. delete rigs, cam, particles ... +1. move objects -This fork is intended to be a starting point of teaching programming in python3 to everyone as I consider the content to be tough but motivational for beginners. -With this in mind I tried to follow ssloys tutorial closely and chose some guidelines: +1. add Decimate modifier to one object +1. select all in viewpane +1. ctrl + l -> Modifier to add last selected modifier in 1. sidepane to all objects +1. set origin to 3d cursor -1. Simplicity over perfection -1. Readability over performance - -To accomplish these goals I included a special linear algebra module `geom.py` instead of using `numpy`. -Numpy is undoubtly more universal and elaborated but harder to read for beginners. -The `geom.py` module contains some basic vector and matrix classes and supports indexing via '.x', '.y' and '.z' notation based on namedtuples. - -After finishing the fork I am going to be working on course material and structured lessons to roll out this exciting course of 3D animation and programming sometime in the future. To motivate students even more I plan providing some short Blender tutorials on how to prepare custom models for this renderer. - -## Here are some examples generated by the renderer - -### Rendered image with shadows, ambient occlusion and specular lighting (current optimum) -Rendered image with shadows and specular lighting - -### Rendered wire mesh -Rendered wire mesh - -### Rendered randomly colored filled mesh triangles -Randomly colored filled mesh triangles +1. select all +1. export obj (triangulate, no obj groups, selected only, write normals) \ No newline at end of file diff --git a/docs/images/ao_map_shade.png b/docs/images/ao_map_shade.png deleted file mode 100644 index caa7149..0000000 Binary files a/docs/images/ao_map_shade.png and /dev/null differ diff --git a/docs/images/diffuse_gouraud_interpolation_fail_wrong_matmul.png b/docs/images/diffuse_gouraud_interpolation_fail_wrong_matmul.png deleted file mode 100644 index b1ea056..0000000 Binary files a/docs/images/diffuse_gouraud_interpolation_fail_wrong_matmul.png and /dev/null differ diff --git a/docs/images/diffuse_gouraud_shade.png b/docs/images/diffuse_gouraud_shade.png deleted file mode 100644 index 3192d45..0000000 Binary files a/docs/images/diffuse_gouraud_shade.png and /dev/null differ diff --git a/docs/images/e04_autumn_mesh.png b/docs/images/e04_autumn_mesh.png deleted file mode 100644 index dd8bf46..0000000 Binary files a/docs/images/e04_autumn_mesh.png and /dev/null differ diff --git a/docs/images/e06_autumn_filled.png b/docs/images/e06_autumn_filled.png deleted file mode 100644 index 172c8ab..0000000 Binary files a/docs/images/e06_autumn_filled.png and /dev/null differ diff --git a/docs/images/e08.1_autumn_flat_shaded.png b/docs/images/e08.1_autumn_flat_shaded.png deleted file mode 100644 index acddf38..0000000 Binary files a/docs/images/e08.1_autumn_flat_shaded.png and /dev/null differ diff --git a/docs/images/e08.2_autumn_zbuffered.png b/docs/images/e08.2_autumn_zbuffered.png deleted file mode 100644 index f24e352..0000000 Binary files a/docs/images/e08.2_autumn_zbuffered.png and /dev/null differ diff --git a/docs/images/e09.1_autumn_perspective.png b/docs/images/e09.1_autumn_perspective.png deleted file mode 100644 index e3029c8..0000000 Binary files a/docs/images/e09.1_autumn_perspective.png and /dev/null differ diff --git a/docs/images/e09.2_autumn_texture.png b/docs/images/e09.2_autumn_texture.png deleted file mode 100644 index cb5ad73..0000000 Binary files a/docs/images/e09.2_autumn_texture.png and /dev/null differ diff --git a/docs/images/estimated_ao_shade.png b/docs/images/estimated_ao_shade.png deleted file mode 100644 index 2fec283..0000000 Binary files a/docs/images/estimated_ao_shade.png and /dev/null differ diff --git a/docs/images/flat_shade.png b/docs/images/flat_shade.png deleted file mode 100644 index 3c7a3a1..0000000 Binary files a/docs/images/flat_shade.png and /dev/null differ diff --git a/docs/images/global_normal_map_shade.png b/docs/images/global_normal_map_shade.png deleted file mode 100644 index edaf768..0000000 Binary files a/docs/images/global_normal_map_shade.png and /dev/null differ diff --git a/docs/images/gouraud_segregated_shade.png b/docs/images/gouraud_segregated_shade.png deleted file mode 100644 index 9b94d30..0000000 Binary files a/docs/images/gouraud_segregated_shade.png and /dev/null differ diff --git a/docs/images/light_z_normalmap_shader.png b/docs/images/light_z_normalmap_shader.png deleted file mode 100644 index b367bf4..0000000 Binary files a/docs/images/light_z_normalmap_shader.png and /dev/null differ diff --git a/docs/images/light_z_normalmap_shader_diffuse.png b/docs/images/light_z_normalmap_shader_diffuse.png deleted file mode 100644 index ed91705..0000000 Binary files a/docs/images/light_z_normalmap_shader_diffuse.png and /dev/null differ diff --git a/docs/images/shadow_buffer.png b/docs/images/shadow_buffer.png deleted file mode 100644 index 0fa5268..0000000 Binary files a/docs/images/shadow_buffer.png and /dev/null differ diff --git a/docs/images/shadow_shade.png b/docs/images/shadow_shade.png deleted file mode 100644 index ee71ccd..0000000 Binary files a/docs/images/shadow_shade.png and /dev/null differ diff --git a/docs/images/specular_map_shading.png b/docs/images/specular_map_shading.png deleted file mode 100644 index 6721a80..0000000 Binary files a/docs/images/specular_map_shading.png and /dev/null differ diff --git a/docs/images/tiny_shader.png b/docs/images/tiny_shader.png deleted file mode 100644 index f3ceedb..0000000 Binary files a/docs/images/tiny_shader.png and /dev/null differ diff --git a/docs/images/wrong_normal_input_use_vertex_point_as_normal_direction.png b/docs/images/wrong_normal_input_use_vertex_point_as_normal_direction.png deleted file mode 100644 index 9cc1d8a..0000000 Binary files a/docs/images/wrong_normal_input_use_vertex_point_as_normal_direction.png and /dev/null differ diff --git a/geom.py b/geom.py index c766d0b..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