From 20402784f774825b16d9b550cecff3c486669178 Mon Sep 17 00:00:00 2001 From: Austin Morlan Date: Thu, 13 Sep 2018 18:57:06 -0700 Subject: [PATCH] Add perspective-correct texture mapping --- include/geometry.h | 18 ++++++- include/loader.h | 1 + src/engine.cpp | 7 ++- src/geometry.cpp | 82 +++++++++++++++++++++++-------- src/loader.cpp | 118 +++++++++++++++++++++++++++++++++++++++------ 5 files changed, 188 insertions(+), 38 deletions(-) diff --git a/include/geometry.h b/include/geometry.h index d6d0a37..792cfbf 100644 --- a/include/geometry.h +++ b/include/geometry.h @@ -14,9 +14,23 @@ struct Material ColorF32 kDiffuse; }; +struct Texture +{ + ColorU32 **texels; + unsigned int width; + unsigned int height; +}; + +struct TextureCoord +{ + float u; + float v; +}; + struct Face { unsigned int vertIndex[3]; + unsigned int textureIndex[3]; Vector normal; ColorF32 color; }; @@ -32,6 +46,7 @@ struct Mesh_LocalData { std::vector verts; std::vector faces; + std::vector uvs; }; struct Mesh_TransformedData @@ -73,7 +88,8 @@ void CullBackfaces( Point &camPosition); void RenderMesh( - Engine_Buffer &buffer, Mesh_TransformedData &mesh, bool smooth); + Engine_Buffer &buffer, Mesh_TransformedData &mesh, bool smooth, Texture &texture, + std::vector &uvs); #define GEOMETRY_H diff --git a/include/loader.h b/include/loader.h index 6e0642a..12bd10f 100644 --- a/include/loader.h +++ b/include/loader.h @@ -4,6 +4,7 @@ int LoadMesh(char *filename, Mesh &mesh); +int LoadTexture(char *filename, Texture &texture); #define LOADER_H diff --git a/src/engine.cpp b/src/engine.cpp index 79f609a..4e06749 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -15,6 +15,7 @@ static Mesh mesh; static Camera camera; static LightList lights; +static Texture texture; // PRIVATE PROTOTYPES @@ -129,10 +130,12 @@ void Engine_Render(Engine_Buffer &buffer, uint32_t input) for (size_t v = 0; v < mesh.transformed.verts.size(); ++v) { mesh.transformed.verts[v].point *= tView * tPersp * tScreen; - mesh.transformed.verts[v].point /= mesh.transformed.verts[v].point.w; + mesh.transformed.verts[v].point.x /= mesh.transformed.verts[v].point.w; + mesh.transformed.verts[v].point.y /= mesh.transformed.verts[v].point.w; + mesh.transformed.verts[v].point.z /= mesh.transformed.verts[v].point.w; } - RenderMesh(buffer, mesh.transformed, mesh.smooth); + RenderMesh(buffer, mesh.transformed, mesh.smooth, texture, mesh.local.uvs); } void Engine_Shutdown(void) diff --git a/src/geometry.cpp b/src/geometry.cpp index 520f06f..967755a 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -66,25 +66,37 @@ void CullBackfaces( } -void RenderMesh(Engine_Buffer &buffer, Mesh_TransformedData &mesh, bool smooth) +void RenderMesh(Engine_Buffer &buffer, Mesh_TransformedData &mesh, bool smooth, Texture &texture, + std::vector &uvs) { for(size_t f = 0; f < mesh.faces.size(); ++f) { + // The vertices of this face unsigned int vIndex0 = mesh.faces[f].vertIndex[0]; unsigned int vIndex1 = mesh.faces[f].vertIndex[1]; unsigned int vIndex2 = mesh.faces[f].vertIndex[2]; + Vertex &v0 = mesh.verts[vIndex0]; + Vertex &v1 = mesh.verts[vIndex1]; + Vertex &v2 = mesh.verts[vIndex2]; - Vertex v0 = mesh.verts[vIndex0]; - Vertex v1 = mesh.verts[vIndex1]; - Vertex v2 = mesh.verts[vIndex2]; - // Bounding box used to for iterating over possible pixels of this triangle + // The UVs of this face's vertices + unsigned int tIndex0 = mesh.faces[f].textureIndex[0]; + unsigned int tIndex1 = mesh.faces[f].textureIndex[1]; + unsigned int tIndex2 = mesh.faces[f].textureIndex[2]; + TextureCoord &t0 = uvs[tIndex0]; + TextureCoord &t1 = uvs[tIndex1]; + TextureCoord &t2 = uvs[tIndex2]; + + + // Bounding box for barycentric calculations (top-left fill convention) BoundingBox box(v0.point, v1.point, v2.point); int yMin = (int)ceilf(box.yMin); int yMax = (int)ceilf(box.yMax) - 1; int xMin = (int)ceilf(box.xMin); int xMax = (int)ceilf(box.xMax) - 1; + // Constants for this triangle used for barycentric calculations Vector v01 = v1.point - v0.point; Vector v02 = v2.point - v0.point; @@ -92,57 +104,87 @@ void RenderMesh(Engine_Buffer &buffer, Mesh_TransformedData &mesh, bool smooth) float dot0102 = Vector::Dot(v01, v02); float dot0202 = Vector::Dot(v02, v02); - // Iterate bounding box and determine if each point is in the triangle + + // Iterate over the bounding box and determine if each point is in the triangle for (int y = yMin; y <= yMax; ++y) { for (int x = xMin; x <= xMax; ++x) { + // Constant terms used for barycentric calculation Point p(x, y, 1.0f); - Vector v0P = p - v0.point; float dot0P01 = Vector::Dot(v0P, v01); float dot0P02 = Vector::Dot(v0P, v02); float denomInv = 1.0f / ((dot0101 * dot0202) - (dot0102 * dot0102)); + + // Calculate the barycentric coordinate of this point float barycenter[3]; barycenter[1] = (dot0202 * dot0P01 - dot0102 * dot0P02) * denomInv; barycenter[2] = (dot0101 * dot0P02 - dot0102 * dot0P01) * denomInv; barycenter[0] = 1.0f - barycenter[1] - barycenter[2]; + // Point is inside the triangle if ( (barycenter[0] >= 0.0f) - && (barycenter[1] >= 0.0f) - && (barycenter[2] >= 0.0f)) + && (barycenter[1] >= 0.0f) + && (barycenter[2] >= 0.0f)) { - ColorF32 totalColor; + // Constant terms used in the U and V interpolation + float a = barycenter[0] * v1.point.w * v2.point.w; + float b = barycenter[1] * v0.point.w * v2.point.w; + float c = barycenter[2] * v0.point.w * v1.point.w; + float abc = 1.0f / (a + b + c); + + // Interpolate U and V + float u = ((a * t0.u) + (b * t1.u) + (c * t2.u)) * abc; + float v = ((a * t0.v) + (b * t1.v) + (c * t2.v)) * abc; + + + // Convert U and V to pixels in the texture image + unsigned int uPixel = (unsigned int)(u * texture.width); + unsigned int vPixel = (unsigned int)(v * texture.height); + + + ColorF32 shading; + + // Gouraud shading - interpolate color based on vertices if (smooth) { - totalColor = + shading = (barycenter[0] * v0.color) + (barycenter[1] * v1.color) + (barycenter[2] * v2.color); } + // Flat shading - base color on single face color else { - totalColor = mesh.faces[f].color; + shading = mesh.faces[f].color; } - ColorU32 color = ColorF32::ConvertToU32(totalColor); + + // Shade the texel with lighting calculations + ColorU32 texel = texture.texels[vPixel][uPixel]; + texel.r *= shading.r; + texel.g *= shading.g; + texel.b *= shading.b; - float z = - (barycenter[0] * v0.point.z) - + (barycenter[1] * v1.point.z) - + (barycenter[2] * v2.point.z); + // Interpolate 1/z for the z-buffer + float zInv = + 1.0f / + ((barycenter[0] * v0.point.w) + + (barycenter[1] * v1.point.w) + + (barycenter[2] * v2.point.w)); - float zInv = 1.0f / z; + + // Draw the pixel if it's closer than what's in the z-buffer int pixel = (y * buffer.width + x); - if (zInv > buffer.zbuffer[pixel]) { - DrawPixel(buffer.buffer, buffer.width, color.u32, x, y); + DrawPixel(buffer.buffer, buffer.width, texel.u32, x, y); buffer.zbuffer[pixel] = zInv; } diff --git a/src/loader.cpp b/src/loader.cpp index 3bb1a16..ca5b808 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -1,6 +1,8 @@ #include "loader.h" #include #include +#include +#include #include @@ -8,6 +10,28 @@ static char *GetLine(char *buffer, int maxLength, FILE *fp); static void ComputeNormals(Mesh &mesh); +#pragma pack(push, 1) +struct BMP_Header +{ + uint16_t fileType; + uint32_t fileSize; + uint16_t reserved0; + uint16_t reserved1; + uint32_t bitmapOffset; + uint32_t size; + int32_t width; + int32_t height; + uint16_t planes; + uint16_t bitsPerPixel; + uint32_t compression; + uint32_t sizeOfBitmap; + int32_t horizRes; + int32_t vertRes; + uint32_t colorsUsed; + uint32_t colorsImportant; +}; +#pragma pack(pop) + // PUBLIC FUNCTIONS int LoadMesh(char *filename, Mesh &mesh) @@ -32,31 +56,52 @@ int LoadMesh(char *filename, Mesh &mesh) { if (token[0] == 'v') { - Vertex v; + if (token[1] == ' ') + { + Vertex v; - sscanf( - token, "%s %f %f %f", - garbage, - &v.point.x, - &v.point.y, - &v.point.z); + sscanf( + token, "%s %f %f %f", + garbage, + &v.point.x, + &v.point.y, + &v.point.z); - mesh.local.verts.push_back(v); + mesh.local.verts.push_back(v); + } + else if (token[1] == 't') + { + TextureCoord textureCoord; + + sscanf( + token, "%s %f %f", + garbage, + &textureCoord.u, + &textureCoord.v); + + mesh.local.uvs.push_back(textureCoord); + } } else if (token[0] == 'f') { Face f; - sscanf(token, "%s %d %d %d", + sscanf(token, "%s %d/%d %d/%d %d/%d", garbage, &f.vertIndex[0], + &f.textureIndex[0], &f.vertIndex[1], - &f.vertIndex[2]); + &f.textureIndex[1], + &f.vertIndex[2], + &f.textureIndex[2]); // Convert to 0-indexed f.vertIndex[0] -= 1; f.vertIndex[1] -= 1; f.vertIndex[2] -= 1; + f.textureIndex[0] -= 1; + f.textureIndex[1] -= 1; + f.textureIndex[2] -= 1; mesh.local.faces.push_back(f); } @@ -66,9 +111,7 @@ int LoadMesh(char *filename, Mesh &mesh) ComputeNormals(mesh); - printf("OBJ: %s\n", filename); - printf("Verts: %lu\n", mesh.local.verts.size()); - printf("Faces: %lu\n", mesh.local.faces.size()); + printf("Mesh: %s [v: %lu f: %lu]\n", filename, mesh.local.verts.size(), mesh.local.faces.size()); mesh.transformed.verts.resize(mesh.local.verts.size()); @@ -77,6 +120,53 @@ int LoadMesh(char *filename, Mesh &mesh) return 0; } +int LoadTexture(char *filename, Texture &texture) +{ + FILE *fp = fopen(filename, "r"); + if (fp == NULL) + { + fprintf(stderr, "Could not open file: %s\n", filename); + return -1; + } + + BMP_Header header = {}; + fread((void*)&header, sizeof(BMP_Header), 1, fp); + fseek(fp, header.bitmapOffset, SEEK_SET); + + texture.texels = (ColorU32**)malloc((size_t)header.height * sizeof(ColorU32*)); + for (int row = 0; row < header.height; ++row) + { + texture.texels[row] = (ColorU32*)calloc(1, (size_t)header.width * sizeof(ColorU32)); + } + + // Padding is added to image to align to 4-byte boundaries + size_t paddingSize = header.width % 4; + uint8_t *padding = (uint8_t*)malloc(paddingSize * sizeof(*padding)); + + for (int y = 0; y < header.height; ++y) + { + for (int x = 0; x < header.width; ++x) + { + fread(&texture.texels[y][x].b, 1, 1, fp); + fread(&texture.texels[y][x].g, 1, 1, fp); + fread(&texture.texels[y][x].r, 1, 1, fp); + } + + // Discard padding byte + if (paddingSize != 0) + { + fread(padding, paddingSize, 1, fp); + } + } + + texture.width = (unsigned int)header.width; + texture.height = (unsigned int)header.height; + + fclose(fp); + + return 0; +} + static char *GetLine(char *buffer, int maxLength, FILE *fp) { while(true) @@ -86,8 +176,6 @@ static char *GetLine(char *buffer, int maxLength, FILE *fp) return NULL; } - // for (length = strlen(buffer), index = 0; isspace(buffer[index]); ++index); - if (buffer[0] != 'v' && buffer[0] != 'f') { continue;