Here's the website link: https://cal-cs184-student.github.io/hw-webpages-sp24-YuntingZh/hw1/index.html
Personal note - basic setup: Install Vcpkg Open a PowerShell / CMD / Git Bash in the folder:..\Documents\CS284A Vcpkg is a package management software that downloads and builds the required software dependencies -> git clone https://github.com/microsoft/vcpkg -> cd vcpkg -> .\bootstrap-vcpkg.bat After vcpkg is installed, install the dependency of this project On 64-bit system (most systems today) ->.\vcpkg.exe install freetype:x64-windows ---- Go to visual studio installer - Modify- Look for "Desktop development with C++ (For CMake support, make sure the "C++ CMake tools for Windows" and any other relevant CMake components are selected.) - CMakeLists.txt, and then click "CMake Settings for Assignment1" no opt for Assignment1 - CMake toolchain file: ..\buildsystems\vcpkg.cmake - Build all - Set youwillknow.cpp as Startup Item - Run the Project: To run the project, you can press F5 or go to Debug > Start Debugging. This will start the application with the debugger attached. If you want to run the project without the debugger, you can press Ctrl + F5 or go to Debug > Start Without Debugging. (got error message when set the wrong main file - unable to start program C:\Users\..\hw0\build\x64-Release\CGL\src\CGL.lib s not a valid Win32 application)
The task involves implementing basic triangle rasterization without supersampling. The goal is to rasterize triangles using the point-in-triangle test for each pixel, with a sample point located at the center of each pixel. The implementation should ensure that the triangle is drawn correctly regardless of its winding order and that no edges are left un-rasterized. The approach involves calculating the triangle's bounding box and iterating over each pixel within the bounding box to perform the point-in-triangle test. The details of the steps are:
In Lecture 2, we discussed how to test whether a point is inside a triangle. A common approach is the vector-based method. For me, the vector method is intuitive, essentially involving a simple check of the cross product values to see if they are on the same side. If a point is on the same side of all edges, then it's inside the triangle. A key aspect of this method is maintaining a consistent direction for the edges, typically clockwise or counterclockwise. If the edges of the triangle are arranged in a consistent direction (such as clockwise or counterclockwise), then this method is very effective. Based on this, I implemented this approach.
bool RasterizerImp::isPointInTriangle(float x, float y, float x0, float y0, float x1, float y1, float x2, float y2) {
// Method 2: Vector cross product(edge function) method
// Convert triangle vertices and point into vectors
float v0x = x1 - x0;
float v0y = y1 - y0;
float v1x = x2 - x1;
float v1y = y2 - y1;
float v2x = x0 - x2;
float v2y = y0 - y2;
// Convert the test point into vectors relative to triangle vertices
float pv0x = x - x0;
float pv0y = y - y0;
float pv1x = x - x1;
float pv1y = y - y1;
float pv2x = x - x2;
float pv2y = y - y2;
// Compute the cross products
float cross0 = v0x * pv0y - v0y * pv0x;
float cross1 = v1x * pv1y - v1y * pv1x;
float cross2 = v2x * pv2y - v2y * pv2x;
// Check the sign of the cross products to determine if the point is on the same side of all edges
if ((cross0 >= 0 && cross1 >= 0 && cross2 >= 0) || (cross0 <= 0 && cross1 <= 0 && cross2 <= 0)) {
return true; // Point is inside the triangle
}
else {
return false; // Point is outside the triangle
}
}
Another method is the barycentric method, which we discussed in Lecture 5. This method can be efficiently
constrained within the triangle's bounding box and can easily handle different winding orders.
bool RasterizerImp::isPointInTriangle(float x, float y, float x0, float y0, float x1, float y1, float x2, float y2) {
// Method 1: Calculate vectors & dot products for barycentric coordinates
float denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
float a = ((y1 - y2) * (x - x2) + (x2 - x1) * (y - y2)) / denom;
float b = ((y2 - y0) * (x - x2) + (x0 - x2) * (y - y2)) / denom;
float c = 1 - a - b;
// Check if point is inside the triangle
return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1;
}
I also encountered a small issue regarding the rounding, which was fixed by using floor and ceiling rounding methods
Fixed - svg3notfilled
I am trying to improve function, and the first thing to do is precompute constants: Compute values that do not change within the loop before entering it. By simply moving 'float denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);' out of my loop, I saved 300 microseconds(I called the timer functions in drawrend::redraw). I'm thinking about how to not check every sample in the bounding box, but honestly, I haven't figured it out yet. I don't know if it's possible to perform Coarse Clipping again before starting to iterate over the bounding box. I've seen suggestions to use hierarchical data structures such as Quadtree or Grid to organize space, which can quickly exclude areas not inside the triangle. Those methods seemed to be especially effective when dealing with a large number of triangles. However, I haven't implemented it yet.
Before
After
The main goal is to reduce the jagged edges (aliasing) that occur when rendering triangles. Supersampling achieves this by taking multiple samples within each pixel, determining how much of the pixel is covered by the triangle, and then averaging these samples to get the final color of the pixel. This approach simulates a higher-resolution rendering, which, when scaled down, produces smoother edges. Approach and Implementation: a. The process starts by calculating the number of samples to be taken within each pixel, determined by the square root of the sample_rate. The samples are spread across a grid within each pixel. For example, with a sample_rate of 4, a 2x2 grid is created inside each pixel. b. Rasterizing with Supersampling: The triangle is rasterized at a higher resolution equivalent to the number of samples. Each sample's color is determined based on whether it falls within the triangle using the point-in-triangle test. c. Resolving Supersamples to Framebuffer: After rasterizing at a higher resolution, the color of each pixel in the framebuffer is computed by averaging the colors of the corresponding samples. This effectively downsamples the higher-resolution image to the display resolution, resulting in smoother edges.
a. Bounding Box Calculation: Identify the minimum and maximum X and Y coordinates among the vertices of the triangle to calculate the bounding box. This step is vital as it narrows down the sample area, making the process more efficient. b. Iterative Pixel Sampling: Loop through each pixel within the bounding box and subdivide it into the number of samples determined by the sample_rate. Calculate each sample's exact position within the pixel using the sampleStep. This nested loop iterates over every pixel within the bounding box and further divides each pixel according to the supersampling rate. It calculates the position of each sample within a pixel (sampleX, sampleY) by offsetting it based on the sampleStep and ensuring the sample point is centered within its portion of the pixel with +0.5f. c. Point-in-Triangle Test for Samples: For each sample, these checks if it lies within the triangle using the isPointInTriangle function. If so, the sample's color is set in sample_buffer. The sample_index calculation ensures that each sample within the supersampled grid is correctly mapped to its position in the buffer. d. Downsampling: Averaging Samples to Final Pixel Colors After rasterizing the scene at a higher resolution using supersampling, this code averages the colors of the supersamples for each pixel to compute the pixel's final color. It goes through the framebuffer pixel by pixel, collects the corresponding supersampled colors, averages them, and writes the result back to the framebuffer in a format suitable for display. This is the "downsampling" step, where the high-resolution supersampled image is reduced back to the framebuffer's resolution.
This implementation of supersampling for antialiasing effectively smooths out jagged edges by averaging multiple samples within each pixel, simulating a higher-resolution rendering that, when scaled back, results in a visually smoother image. Once the code can be run, test4.svg can be rendered. The differences in the image quality are due to the varying levels of supersampling used in rendering the SVGs.
Sample Rate 1: This is essentially no supersampling at all. Each pixel is sampled once, and its color is determined by whether the center of the pixel lies within the triangle. This results in sharper edges because there's no averaging of color. The pixel is either fully the triangle's color or not, creating a jagged appearance known as aliasing. Sample Rate 4: This involves supersampling with 4 samples per pixel, arranged in a 2x2 grid within each pixel. Each sample is tested to see if it lies within the triangle. The final color of the pixel is an average of these samples. This begins to smooth out the edges of the triangles, as the color of the pixels at the edge of the triangle is a mix of the triangle color and the background, depending on how many samples are inside the triangle. Sample Rate 16: With 16 samples per pixel arranged in a 4x4 grid, there's an even more significant averaging effect. The edges of the triangles become much smoother (or "blurrier") as more samples around the edge contribute to the average, creating a gradient effect from the color of the triangle to the background. This dramatically reduces the jagged edges and can make the image appear less sharp, as finer details may be lost in the averaging process. In summary, the increase in sample rate smooths out the edges by averaging more samples within each pixel, which blends the colors at the boundaries of the triangles. However, the trade-off is that higher sample rates can make the image look less appealing as the pixel colors become a mix of the triangle and background colors.
Task 3 requires me to implement the three transforms.
I started by using an affine
transformation matrix, more specifically, a translation matrix:
Matrix3x3(1, 0, dx,
0, 1, dy,
0, 0, 1);
According to the class content: When this matrix is applied to a vector or a point in 2D space, it translates (moves) the point by dx units along the X-axis and dy units along the Y-axis.
Then, I created a 3x3 matrix for scaling transformations in a 2D space:
Matrix3x3(sx, 0, 0,
0, sy, 0,
0, 0, 1);
When this scaling matrix is applied to a point or a vector in 2D space, it scales (enlarges or shrinks) the coordinates of that point. If sx and sy are greater than 1, the point will be scaled up (enlarged); if they are between 0 and 1, the point will be scaled down (shrunk).
The third part I need to implement is the rotate(float deg)
function, rotating an object
around the origin in a 2D plane. As discussed in class, the equation I need to use is as follows:
Matrix3x3(cos_theta, -sin_theta, 0,
sin_theta, cos_theta, 0,
0, 0, 1);
Now that I want to my robot to wave his hands. In my SVG, the last two
To adjust the lower arm so that it aligns correctly with the upper arm, I need to consider that the initial rotation point and the point where the lower arm should be connected have both moved due to the upper arm's rotation and translation
Let me explain how I did this. I set the upper arm's translate to (-90 -80)compared to the previous(90 -40), which now moves the element 40 units in the negative y-direction (visually upwards), making it higher. This is because when I initially rotated it 90 degrees, I noticed that the position of the upper arm was slightly low, so I moved it up by 40 units. In the transforms.cpp, my rotate function defines a 2D rotation matrix that only takes an angle as a parameter, defaulting to rotate around the origin (0, 0). This matrix does not directly specify a center of rotation; it's a transformation matrix that rotates around the coordinate origin. Through multiple trials and adjustments, I found that moving the lower arm 70 units up along the y-axis looked better, so I used translate(0, -70). Similarly, I modified the right arm, so the final effect is as follows. Specific data can be seen in my_robot.svg in my docs/ directory.
Barycentric coordinates represent a system for expressing the location of a point within a triangle relative to its vertices. In this system, the position of any point can be described as a combination of weights assigned to each vertex of the triangle. These weights indicate how much influence each vertex has on the final position of the point, and they always add up to one to ensure the point remains within the confines of the triangle. Barycentric coordinates are particularly useful for interpolating values across the surface of a triangle. This is essential for tasks such as color blending, where the colors defined at the vertices of a triangle need to be smoothly transitioned across its interior.
To rasterize a triangle onto the screen, first, the triangle's vertices are given by (x0, y0), (x1, y1), and (x2, y2). Each vertex has an associated color c0, c1, and c2. The colors of the interior pixels are interpolated based on the vertices' colors using barycentric coordinates. I have already mentioned this method of Barycentric interpolation in task 1. Basically, it still involves first creating a bounding box and then calculating the three points alpha, beta, and gamma. my function rasterize_interpolated_color_triangle,I perform the >= 0 check, and by using gamma = 1.0f - alpha - beta, I have implicitly included the <= 1. Final step is Interpolate the color and fill the pixel Once we've confirmed the pixel is inside the triangle, we interpolate the color based on the barycentric coordinates. The fill_pixel function colors the pixel at (x, y) with the interpolated color. If you're using supersampling, ensure that fill_pixel properly combines the supersampled colors.
Pixel sampling is like deciding how to mix your paint colors when you're zooming in on a tiny part of your painting. It wants the colors to blend nicely, so there are no harsh lines or blocky pixels. There are two types of pixel sampling:
Color Texture::sample_nearest(Vector2D uv, int level) {
// Check if the mipmap level is valid
if (level < 0 || level >= mipmap.size()) {
// Invalid level, return magenta
return Color(1, 0, 1);
}
auto& mip = mipmap[level];
int width = mip.width;
int height = mip.height;
// Convert (u, v) to texel space
int u = static_cast(uv.x * width - 0.5f);
int v = static_cast(uv.y * height - 0.5f);
// Clamp coordinates to the texture dimensions
u = std::max(0, std::min(u, width - 1));
v = std::max(0, std::min(v, height - 1));
// Retrieve and return the texel color
return mip.get_texel(u, v);
}
Color Texture::sample_bilinear(Vector2D uv, int level) {
//Task5
if (level < 0 || level >= mipmap.size()) {
// Invalid level, return magenta
return Color(1, 0, 1);
}
auto& mip = mipmap[level];
int width = mip.width;
int height = mip.height;
// Convert (u, v) to texel space
float u = uv.x * width;
float v = uv.y * height;
// Calculate the texel indices
int u1 = floor(u);
int v1 = floor(v);
int u2 = u1 + 1;
int v2 = v1 + 1;
// Clamp to texture dimensions to avoid overflow
u1 = std::max(0, std::min(u1, width - 1));
v1 = std::max(0, std::min(v1, height - 1));
u2 = std::max(0, std::min(u2, width - 1));
v2 = std::max(0, std::min(v2, height - 1));
// Retrieve the texel colors
Color c00 = mip.get_texel(u1, v1);
Color c10 = mip.get_texel(u2, v1);
Color c01 = mip.get_texel(u1, v2);
Color c11 = mip.get_texel(u2, v2);
// Calculate fractional parts for interpolation
float s = u - u1;
float t = v - v1;
// Perform bilinear interpolation
Color c0 = c00 * (1 - s) + c10 * s; // Interpolate along u
Color c1 = c01 * (1 - s) + c11 * s; // Interpolate along u
Color c = c0 * (1 - t) + c1 * t; // Interpolate along v
return c;
// return magenta for invalid level
//return Color(1, 0, 1);
}
Level sampling is a technique used to handle how textures are displayed at different distances and angles relative to the viewer, which related to Mipmapping and texture filtering.Problem with Direct Texture Mapping: If I have a detailed texture (like a high-resolution image) applied to a surface in a 3D scene. When this surface is far away or viewed at a steep angle, the texture's details can't be correctly displayed on the screen due to limited screen resolution and pixel density. This mismatch often causes visual artifacts like moiré patterns and aliasing. That's why I should use Mipmapping, which involves creating multiple versions of the same texture at progressively lower resolutions. I want to explain some tradeoffs between speed, memory usage, and antialiasing power between the various techniques that we used in this task:
In task 6, I first moved the part of my previous logic that checks if it's linear or nearest into the Texture::sample. Then, in the rasterize_textured_triangle, I make sure to scale up the difference vectors accordingly by the width and height of the full-resolution texture image.
void RasterizerImp::rasterize_textured_triangle(float x0, float y0, float u0, float v0,
float x1, float y1, float u1, float v1,
float x2, float y2, float u2, float v2,
Texture& tex)
{
// TODO: Task 5: Fill in the SampleParams struct and pass it to the tex.sample function.
// Bilinear interpolation:
BoundingBox bbox = calculateBoundingBox(x0, y0, x1, y1, x2, y2);
for (float x = bbox.minX; x <= bbox.maxX; x++) { for (float y=bbox.minY; y <=bbox.maxY; y++) { // Calculate barycentric
coordinates for the current pixel float alpha=((y1 - y2) * (x - x2) + (x2 - x1) * (y - y2)) / ((y1 - y2) * (x0 - x2)
+ (x2 - x1) * (y0 - y2)); float beta=((y2 - y0) * (x - x2) + (x0 - x2) * (y - y2)) / ((y1 - y2) * (x0 - x2) + (x2 -
x1) * (y0 - y2)); float gamma=1.0f - alpha - beta; // Check if the pixel is inside the triangle if (alpha>= 0 &&
beta >= 0 && gamma >= 0) {
// Interpolate texture coordinates using barycentric coordinates
float u = alpha * u0 + beta * u1 + gamma * u2;
float v = alpha * v0 + beta * v1 + gamma * v2;
// TODO: Task 6: Set the correct barycentric differentials in the SampleParams struct.
// Hint: You can reuse code from rasterize_triangle/rasterize_interpolated_color_triangle
// Approximate derivatives based on vertex differences
float dudx = (u1 - u0) / (x1 - x0);
float dvdx = (v1 - v0) / (x1 - x0);
float dudy = (u2 - u0) / (y2 - y0);
float dvdy = (v2 - v0) / (y2 - y0);
SampleParams sp;
sp.p_uv = Vector2D(u, v);
sp.p_dx_uv = Vector2D(dudx, dvdx) * tex.mipmap[0].width;
sp.p_dy_uv = Vector2D(dudy, dvdy) * tex.mipmap[0].height;
sp.lsm = lsm;
sp.psm = psm;
//=====================
Color color = tex.sample(sp);
fill_pixel(x, y, color);
}
}
}
}
I implement get_level function accroding to slides:
float Texture::get_level(const SampleParams& sp) {
float dudx = sp.p_dx_uv.x;
float dvdx = sp.p_dx_uv.y;
float dudy = sp.p_dy_uv.x;
float dvdy = sp.p_dy_uv.y;
float gradX = sqrt(dudx * dudx + dvdx * dvdx);
float gradY = sqrt(dudy * dudy + dvdy * dvdy);
float maxGrad = std::max(gradX, gradY);
float level = log2(maxGrad);//D = log2L
if (level < 0) {
level = 0;
}
return level;
}
The purpose of this function is to calculate the appropriate Mipmap level, which is typically based on the rate of change of the texture coordinates. This involves estimating the derivatives of the texture coordinates in screen space, which can be used to assess the degree of texture variation at a pixel, thereby selecting the appropriate Mipmap level. First, I calculate the derivatives of the texture coordinates in the x and y directions of screen space. Then, I calculate the magnitude of the gradients in these two directions and choose the larger of the two as my gradient. After that, I use the log2 function to calculate the Mipmap level. The log2 function used here is based on the magnitude of the gradient, which can represent the rate of texture change on the screen. Finally, I ensure that the returned level is not negative, as Mipmap levels cannot be negative.
In the texture::sample, different levels can be selected: L_ZERO = 0, L_NEAREST = 1, L_LINEAR = 2. And inside sp.lsm == L_LINEAR, I did a trilinear filtering. I interpolate between two adjacent Mipmap levels to obtain the final texture color. Here is the comparison of my final results:
Task 6 L_ZERO P_LINEAR
Task 6 L_NEAREST P_LINEAR
Task 6 L_LINEAR P_LINEAR
Task 6 L_ZERO P_NEAREST
Task 6 L_NEAREST P_NEAREST
Task 6 L_LINEAR P_NEAREST
UPDATE:Here's my own texture PNG file:
Task 6 L_ZERO P_NEAREST
Task 6 L_ZERO P_LINEAR
Task 6 L_NEAREST P_NEAREST
Task 6 L_NEAREST P_LINEAR