|
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