|
2208 | 2208 | <div class='together'> |
2209 | 2209 | ... Indeed we do get rather nice gray spheres: |
2210 | 2210 |
|
2211 | | -  |
| 2211 | +  |
2213 | 2213 |
|
2214 | 2214 | </div> |
2215 | 2215 |
|
|
2269 | 2269 |
|
2270 | 2270 | if (world.hit(r, interval(0, infinity), rec)) { |
2271 | 2271 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
2272 | | - point3 target = rec.p + rec.normal + random_in_unit_sphere(); |
2273 | | - return 0.5 * ray_color(ray(rec.p, target - rec.p), depth-1, world); |
| 2272 | + vec3 direction = random_on_hemisphere(rec.normal); |
| 2273 | + return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); |
2274 | 2274 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
2275 | 2275 | } |
2276 | 2276 |
|
|
2308 | 2308 | <div class='together'> |
2309 | 2309 | For this very simple scene we should get basically the same result: |
2310 | 2310 |
|
2311 | | -  |
2313 | 2313 |
|
2314 | 2314 | </div> |
2315 | 2315 |
|
2316 | 2316 |
|
2317 | | -Using Gamma Correction for Accurate Color Intensity |
2318 | | ----------------------------------------------------- |
2319 | | -Note the shadowing under the sphere. The picture is very dark, but our spheres only absorb half the |
2320 | | -energy of each bounce, so they are 50% reflectors. The spheres should look pretty bright (in real |
2321 | | -life, a light grey) but they appear to be rather dark. The reason for this is that almost all |
2322 | | -computer programs assume that an image is “gamma corrected” before being written into an image |
2323 | | -file. This means that the 0 to 1 values have some transform applied before being stored as a byte. |
2324 | | -Images with data that are written without being transformed are said to be in _linear space_, |
2325 | | -whereas images that are transformed are said to be in _gamma space_. It is likely that the image |
2326 | | -viewer you are using is expecting an image in gamma space, but we are giving it an image in linear |
2327 | | -space. This is the reason why our image appears inaccurately dark. |
2328 | | - |
2329 | | -There are many good reasons for why images should be stored in gamma space, but for our purposes we |
2330 | | -just need to be aware of it. We are going to transform our data into gamma space so that our image |
2331 | | -viewer can more accurately display our image. As a simple approximation, we can use “gamma 2” as our |
2332 | | -transform, which is the power that you use when going from gamma space to linear space. We need to |
2333 | | -go from linear space to gamma space, which means taking the inverse of "gamma 2", so the power |
2334 | | -$1/\mathit{gamma}$, which is just the square-root. |
2335 | | - |
2336 | | - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
2337 | | - inline double linear_to_gamma(double linear_component) |
2338 | | - { |
2339 | | - return sqrt(linear_component); |
2340 | | - } |
2341 | | - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
2342 | | - |
2343 | | - void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) { |
2344 | | - auto r = pixel_color.x(); |
2345 | | - auto g = pixel_color.y(); |
2346 | | - auto b = pixel_color.z(); |
2347 | | - |
2348 | | - // Divide the color by the number of samples. |
2349 | | - auto scale = 1.0 / samples_per_pixel; |
2350 | | - r *= scale; |
2351 | | - g *= scale; |
2352 | | - b *= scale; |
2353 | | - |
2354 | | - |
2355 | | - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
2356 | | - // Apply the linear to gamma transform. |
2357 | | - r = linear_to_gamma(r); |
2358 | | - g = linear_to_gamma(g); |
2359 | | - b = linear_to_gamma(b); |
2360 | | - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
2361 | | - |
2362 | | - // Write the translated [0,255] value of each color component. |
2363 | | - static const interval intensity(0.000, 0.999); |
2364 | | - out << static_cast<int>(256 * intensity.clamp(r)) << ' ' |
2365 | | - << static_cast<int>(256 * intensity.clamp(g)) << ' ' |
2366 | | - << static_cast<int>(256 * intensity.clamp(b)) << '\n'; |
2367 | | - } |
2368 | | - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
2369 | | - [Listing [write-color-gamma]: <kbd>[color.h]</kbd> write_color(), with gamma correction] |
2370 | | - |
2371 | | -<div class='together'> |
2372 | | -This yields light grey, which is more accurate: |
2373 | | - |
2374 | | -  |
2376 | | - |
2377 | | -</div> |
2378 | | - |
2379 | | - |
2380 | 2317 | Fixing Shadow Acne |
2381 | 2318 | ------------------- |
2382 | 2319 | There’s also a subtle bug that we need to address. A ray will attempt to accurately calculate the |
|
2405 | 2342 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
2406 | 2343 | if (world.hit(r, interval(0.001, infinity), rec)) { |
2407 | 2344 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
2408 | | - point3 target = rec.p + rec.normal + random_in_unit_sphere(); |
2409 | | - return 0.5 * ray_color(ray(rec.p, target - rec.p), depth-1, world); |
| 2345 | + vec3 direction = random_on_hemisphere(rec.normal); |
| 2346 | + return 0.5 * ray_color(ray(rec.p, direction), depth-1, world); |
2410 | 2347 | } |
2411 | 2348 |
|
2412 | 2349 | vec3 unit_direction = unit_vector(r.direction()); |
|
2487 | 2424 | <div class='together'> |
2488 | 2425 | After rendering we get a similar image: |
2489 | 2426 |
|
2490 | | -  |
2492 | 2429 |
|
2493 | 2430 | </div> |
|
2496 | 2433 | spheres is so simple, but you should be able to notice two important visual differences: |
2497 | 2434 |
|
2498 | 2435 | 1. The shadows are more pronounced after the change |
2499 | | - 2. Both spheres are darker in appearance after the change |
| 2436 | + 2. Both spheres are tinted blue from the sky after the change |
2500 | 2437 |
|
2501 | 2438 | Both of these changes are due to the less uniform scattering of the light rays--more rays are |
2502 | 2439 | scattering toward the normal. This means that for diffuse objects, they will appear _darker_ |
|
2510 | 2447 | insight by understanding the effect of different diffuse methods on the lighting of the scene. |
2511 | 2448 |
|
2512 | 2449 |
|
| 2450 | +Using Gamma Correction for Accurate Color Intensity |
| 2451 | +---------------------------------------------------- |
| 2452 | +Note the shadowing under the sphere. The picture is very dark, but our spheres only absorb half the |
| 2453 | +energy of each bounce, so they are 50% reflectors. The spheres should look pretty bright (in real |
| 2454 | +life, a light grey) but they appear to be rather dark. We can see this more clearly if we walk |
| 2455 | +through the full brightness gamut for our diffuse material. We start by setting the reflectance of |
| 2456 | +the `ray_color` function from `0.5` (50%) to `0.1` (10%): |
| 2457 | + |
| 2458 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
| 2459 | +class camera { |
| 2460 | + ... |
| 2461 | + color ray_color(const ray& r, int depth, const hittable& world) const { |
| 2462 | + hit_record rec; |
| 2463 | + |
| 2464 | + // If we've exceeded the ray bounce limit, no more light is gathered. |
| 2465 | + if (depth <= 0) |
| 2466 | + return color(0,0,0); |
| 2467 | + |
| 2468 | + if (world.hit(r, interval(0.001, infinity), rec)) { |
| 2469 | + vec3 direction = rec.normal + random_unit_vector(); |
| 2470 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
| 2471 | + return 0.1 * ray_color(ray(rec.p, direction), depth-1, world); |
| 2472 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
| 2473 | + } |
| 2474 | + |
| 2475 | + vec3 unit_direction = unit_vector(r.direction()); |
| 2476 | + auto a = 0.5*(unit_direction.y() + 1.0); |
| 2477 | + return (1.0-a)*color(1.0, 1.0, 1.0) + a*color(0.5, 0.7, 1.0); |
| 2478 | + } |
| 2479 | +}; |
| 2480 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 2481 | +[Listing [ray-color-gamut]: <kbd>[camera.h]</kbd> ray_color() with 10% reflectance] |
| 2482 | + |
| 2483 | +We render out at this new 10% reflectance. We then set reflectance to 30% and render again. We |
| 2484 | +repeat for 50%, 70%, and finally 90%. We can overlay these images from left to right in the photo |
| 2485 | +editor of your choice and you should get a very nice visual representation of the increasing |
| 2486 | +brightness of your chosen gamut. This is the one that we've been working with so far: |
| 2487 | + |
| 2488 | + |
| 2490 | + |
| 2491 | +If you look closely, or if you use a color picker, you should notice that the 50% reflectance |
| 2492 | +render (the one in the middle) is far too dark to be half-way between white and black (middle-gray). |
| 2493 | +Indeed, the 70% reflector is closer to middle-gray. The reason for this is that almost all |
| 2494 | +computer programs assume that an image is “gamma corrected” before being written into an image |
| 2495 | +file. This means that the 0 to 1 values have some transform applied before being stored as a byte. |
| 2496 | +Images with data that are written without being transformed are said to be in _linear space_, |
| 2497 | +whereas images that are transformed are said to be in _gamma space_. It is likely that the image |
| 2498 | +viewer you are using is expecting an image in gamma space, but we are giving it an image in linear |
| 2499 | +space. This is the reason why our image appears inaccurately dark. |
| 2500 | + |
| 2501 | +There are many good reasons for why images should be stored in gamma space, but for our purposes we |
| 2502 | +just need to be aware of it. We are going to transform our data into gamma space so that our image |
| 2503 | +viewer can more accurately display our image. As a simple approximation, we can use “gamma 2” as our |
| 2504 | +transform, which is the power that you use when going from gamma space to linear space. We need to |
| 2505 | +go from linear space to gamma space, which means taking the inverse of "gamma 2", which means an |
| 2506 | +exponent of $1/\mathit{gamma}$, which is just the square-root. |
| 2507 | + |
| 2508 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
| 2509 | + inline double linear_to_gamma(double linear_component) |
| 2510 | + { |
| 2511 | + return sqrt(linear_component); |
| 2512 | + } |
| 2513 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
| 2514 | + |
| 2515 | + void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) { |
| 2516 | + auto r = pixel_color.x(); |
| 2517 | + auto g = pixel_color.y(); |
| 2518 | + auto b = pixel_color.z(); |
| 2519 | + |
| 2520 | + // Divide the color by the number of samples. |
| 2521 | + auto scale = 1.0 / samples_per_pixel; |
| 2522 | + r *= scale; |
| 2523 | + g *= scale; |
| 2524 | + b *= scale; |
| 2525 | + |
| 2526 | + |
| 2527 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ highlight |
| 2528 | + // Apply the linear to gamma transform. |
| 2529 | + r = linear_to_gamma(r); |
| 2530 | + g = linear_to_gamma(g); |
| 2531 | + b = linear_to_gamma(b); |
| 2532 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C++ |
| 2533 | + |
| 2534 | + // Write the translated [0,255] value of each color component. |
| 2535 | + static const interval intensity(0.000, 0.999); |
| 2536 | + out << static_cast<int>(256 * intensity.clamp(r)) << ' ' |
| 2537 | + << static_cast<int>(256 * intensity.clamp(g)) << ' ' |
| 2538 | + << static_cast<int>(256 * intensity.clamp(b)) << '\n'; |
| 2539 | + } |
| 2540 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 2541 | + [Listing [write-color-gamma]: <kbd>[color.h]</kbd> write_color(), with gamma correction] |
| 2542 | + |
| 2543 | + |
| 2544 | +<div class='together'> |
| 2545 | +Using this gamma correction, we now get a much more consistent ramp from darkness to lightness: |
| 2546 | + |
| 2547 | +  |
| 2549 | + |
| 2550 | +</div> |
2513 | 2551 |
|
2514 | 2552 |
|
2515 | 2553 | Metal |
|
2840 | 2878 | <div class='together'> |
2841 | 2879 | Which gives: |
2842 | 2880 |
|
2843 | | -  |
| 2881 | +  |
2845 | 2883 |
|
2846 | 2884 | </div> |
2847 | 2885 |
|
|
2903 | 2941 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
2904 | 2942 | [Listing [metal-fuzz-spheres]: <kbd>[main.cc]</kbd> Metal spheres with fuzziness] |
2905 | 2943 |
|
2906 | | -  |
| 2944 | +  |
2908 | 2946 |
|
2909 | 2947 | </div> |
2910 | 2948 |
|
|
2922 | 2960 | there is a refraction ray at all. For this project, I tried to put two glass balls in our scene, and |
2923 | 2961 | I got this (I have not told you how to do this right or wrong yet, but soon!): |
2924 | 2962 |
|
2925 | | -  |
| 2963 | +  |
2927 | 2965 |
|
2928 | 2966 | Is that right? Glass balls look odd in real life. But no, it isn’t right. The world should be |
2929 | 2967 | flipped upside down and no weird black stuff. I just printed out the ray straight through the middle |
|
3035 | 3073 | <div class='together'> |
3036 | 3074 | This gives us the following result: |
3037 | 3075 |
|
3038 | | -  |
| 3076 | +  |
3040 | 3078 |
|
3041 | 3079 | </div> |
3042 | 3080 |
|
|
3155 | 3193 | <div class='together'> |
3156 | 3194 | We get: |
3157 | 3195 |
|
3158 | | -  |
| 3196 | +  |
3160 | 3198 |
|
3161 | 3199 | </div> |
3162 | 3200 |
|
|
3235 | 3273 | <div class='together'> |
3236 | 3274 | This gives: |
3237 | 3275 |
|
3238 | | -  |
| 3276 | +  |
3240 | 3278 |
|
3241 | 3279 | </div> |
3242 | 3280 |
|
|
3358 | 3396 | <div class='together'> |
3359 | 3397 | This gives us the rendering: |
3360 | 3398 |
|
3361 | | -  |
| 3399 | +  |
3363 | 3401 |
|
3364 | 3402 | </div> |
3365 | 3403 |
|
|
3516 | 3554 | <div class='together'> |
3517 | 3555 | to get: |
3518 | 3556 |
|
3519 | | -  |
| 3557 | +  |
3521 | 3559 |
|
3522 | 3560 | </div> |
3523 | 3561 |
|
|
3534 | 3572 | <div class='together'> |
3535 | 3573 | to get: |
3536 | 3574 |
|
3537 | | -  |
| 3575 | +  |
3538 | 3576 |
|
3539 | 3577 | </div> |
3540 | 3578 |
|
|
3782 | 3820 | <div class='together'> |
3783 | 3821 | We get: |
3784 | 3822 |
|
3785 | | -  |
| 3823 | +  |
3787 | 3825 |
|
3788 | 3826 | </div> |
3789 | 3827 |
|
|
3868 | 3906 | <div class='together'> |
3869 | 3907 | This gives: |
3870 | 3908 |
|
3871 | | -  |
| 3909 | +  |
3872 | 3910 |
|
3873 | 3911 | </div> |
3874 | 3912 |
|
|
0 commit comments