Compare commits
10 commits
149d220d5b
...
81becdf0d7
Author | SHA1 | Date | |
---|---|---|---|
81becdf0d7 | |||
64449e18f7 | |||
95c7fcaa33 | |||
ed4902739a | |||
81ca532446 | |||
1f8d6adae3 | |||
6de86045ba | |||
d167c2074e | |||
971bfc6394 | |||
eda62c335b |
3 changed files with 809934 additions and 89798 deletions
37
README.md
37
README.md
|
@ -81,7 +81,7 @@ A diffuse object is any object that is not emitting light, takes on the colours
|
|||
|
||||
I generated random reflection vectors and got the following (this is the first time a shadow can be observed, left one is mine, right one from the book - note the stripe is probably a consequence of fast inverse square and of using 32-bit floats instead of 64-bit ones):
|
||||
|
||||
<img src="https://media.discordapp.net/attachments/929667122404155445/1212496739668598875/image.png?ex=65f20c95&is=65df9795&hm=d8db91eee8a0f62250c1c64628e7d214aa39c504b94ffa13a554335addfb0905&=&format=webp&quality=lossless&width=1148&height=655" width="480"/><img src="https://raytracing.github.io/images/img-1.07-first-diffuse.png" width="480"/>
|
||||
<img src="https://preview.redd.it/some-projects-ive-been-working-on-3-v0-xondbx2r1emc1.png?width=1080&crop=smart&auto=webp&s=2cd684d53024c5756c7459e3762723faefb00182" width="480"/><img src="https://raytracing.github.io/images/img-1.07-first-diffuse.png" width="480"/>
|
||||
|
||||
A problem that occurs on this image is the **shadow acne** problem: A ray will attempt to accurately calculate the intersection point when it intersects with a surface. Unfortunately for us, this calculation is susceptible to floating point rounding errors which can cause the intersection point to be ever so slightly off. This means that the origin of the next ray, the ray that is randomly scattered off of the surface, is unlikely to be perfectly flush with the surface. It might be just above the surface. It might be just below the surface. If the ray's origin is just below the surface then it could intersect with that surface again. Which means that it will find the nearest surface at t=0.00000001 or whatever floating point approximation the hit function gives us. The simple fix yields this image:
|
||||
|
||||
|
@ -93,7 +93,7 @@ To make the shadows look more realistic, I had to implement the Lambertian refle
|
|||
|
||||
The last issue left in the chapter is the darkness of the image and it's due to some strange linear-gamma space conversion? I don't quite understand how that works, but by taking the square root of the colour values we get the right brightness. Nice
|
||||
|
||||
<img src="https://media.discordapp.net/attachments/874752364698013736/1212767017711697950/image.png?ex=65f3084d&is=65e0934d&hm=b8eabd56619cff0984e361c4917b9f63b7d7e3a76e28dbc666c7d91f9f6331de&=&format=webp&quality=lossless&width=720&height=405" width="480"/>
|
||||
<img src="https://preview.redd.it/some-projects-ive-been-working-on-3-v0-a4a1203r1emc1.png?width=914&format=png&auto=webp&s=f14960bbbb7bc99d9209b6a84c1a9a7c99cd4c9f" width="480"/>
|
||||
|
||||
### Chapter 8: The Materials struct and Metals
|
||||
|
||||
|
@ -105,8 +105,37 @@ Most metals however aren't purely reflective, they fuzz the reflection a little
|
|||
|
||||
<img src="https://raytracing.github.io/images/img-1.14-metal-fuzz.png" width="480"/>
|
||||
|
||||
### Chapter 9: Glass
|
||||
### Chapter 9: Glass and Slowness
|
||||
|
||||
In this chapter glass and other dielectric materials have been implemented using Snell's law which describes refraction. The first attempt yields the following rendering:
|
||||
|
||||
<img src="https://raytracing.github.io/images/img-1.16-glass-always-refract.png" width="480"/>
|
||||
<img src="https://raytracing.github.io/images/img-1.16-glass-always-refract.png" width="480"/>
|
||||
|
||||
In the later part I implemented Schlick's Approximation which approximates the reflectivity of dielectric objects depending on the angle.
|
||||
|
||||
Here I got some strange result (actually I already noticed that the previous render had a strange cyan sheen on the upper part of the ball, but dismissed it as a result of inaccuracies).
|
||||
|
||||
I oddly enough got this wrong result, when trying to render the hollow ball:
|
||||
<img src="https://preview.redd.it/some-projects-ive-been-working-on-3-v0-7biqlfxt1emc1.png?width=570&format=png&auto=webp&s=7931c97310e94b91b0ca363d7ce83d5bfeb7524f" width="480"/>
|
||||
|
||||
It took me some time to find the error and believe it or not, it was the 3% inaccuracy caused by the fast inverse square algorithm! I had to remove it and replace it with a more simple and accurate but slower calculation. The slow down is small but visible! Now I get:
|
||||
|
||||
<img src="https://raytracing.github.io/images/img-1.18-glass-hollow.png" width="480"/>
|
||||
|
||||
### Chapters 10 and 11: Extra camera utilities and depth of field (blur)
|
||||
|
||||
I implemented the camera to be movable and also simulated the lens blur effect we see in cameras. The blur effect can be emulated by implemented the following six steps:
|
||||
1. The focus plane is orthogonal to the camera view direction.
|
||||
2. The focus distance is the distance between the camera center and the focus plane.
|
||||
3. The viewport lies on the focus plane, centered on the camera view direction vector.
|
||||
4. The grid of pixel locations lies inside the viewport (located in the 3D world).
|
||||
5. Random image sample locations are chosen from the region around the current pixel location.
|
||||
6. The camera fires rays from random points on the lens through the current image sample location. I get the following output:
|
||||
|
||||
<img src="https://preview.redd.it/more-progress-3-v0-w1iblg591dnc1.png?width=575&format=png&auto=webp&s=68dbbcdf9ae54d625664019f6bd7189367de7be9" width="480"/>
|
||||
|
||||
### Project complete!
|
||||
|
||||
Rendering the final image took quite a bit of time, but I'm glad the whole program finally works. I had to spend quite a lot of time to try to fix the glass - I still don't know exactly what's gone wrong, but to fix the issue I simply decided to (vertically) flip the reflected image of glass balls which magically fixed the code (the glass-balls are rendered correctly but their reflections are of course flipped, though in a way they appear more natural this way). This is the final render of my project (I translated the final scene from the guide into Go):
|
||||
|
||||
<img src="https://preview.redd.it/final-result-v0-t6z7744lfjnc1.png?width=640&crop=smart&auto=webp&s=44dd44ceedd87f6fa84ab2ccaf9390163bfaf23b" width="480"/>
|
207
main.go
207
main.go
|
@ -119,10 +119,21 @@ func q_rsqrt(v Vec3) float32 {
|
|||
}
|
||||
|
||||
func Unit_vector(v Vec3) Vec3 {
|
||||
new_v := v.Mult(q_rsqrt(v))
|
||||
//new_v := v.Mult(q_rsqrt(v))
|
||||
new_v := v.Mult(1.0/v.Length())
|
||||
return new_v
|
||||
}
|
||||
|
||||
func Random_in_unit_disk() Vec3 {
|
||||
for true {
|
||||
p := NewVec3(RandomDoubleInRange(-1,1), RandomDoubleInRange(-1,1), 0)
|
||||
if p.Length_squared() < 1 {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return NewVec3(0,0,0)
|
||||
}
|
||||
|
||||
func RandomInUnitSphere() Vec3 {
|
||||
for true {
|
||||
p := RandomVec3(-1, 1)
|
||||
|
@ -150,17 +161,17 @@ func Reflect(v Vec3, n Vec3) Vec3 {
|
|||
return v.Sub(n.Mult(2*Dot(v,n)))
|
||||
}
|
||||
|
||||
func Refract(uv Vec3, n Vec3, etai_over_etat float32) Vec3 {
|
||||
cos_theta := float32(math.Min(float64(Dot(uv.Neg(), n)), 1.0))
|
||||
var r_out_perp Vec3 = (uv.Add(n.Mult(cos_theta))).Mult(etai_over_etat)
|
||||
var r_out_parallel Vec3 = n.Mult(float32(-math.Sqrt(math.Abs(float64(1.0 - r_out_perp.Length_squared())))))
|
||||
return r_out_perp.Add(r_out_parallel)
|
||||
func Refract(uv, n Vec3, etai_over_etat float32) Vec3 {
|
||||
cos_theta := float32(math.Min(float64(Dot(uv.Neg(), n)), 1.0))
|
||||
r_out_perp := uv.Add(n.Mult(cos_theta)).Mult(etai_over_etat)
|
||||
r_out_parallel := n.Mult(float32(math.Sqrt(math.Abs(1.0 - float64(r_out_perp.Length_squared())))))
|
||||
return r_out_perp.Add(r_out_parallel)
|
||||
}
|
||||
|
||||
const pi float32 = 3.1415926535897932385
|
||||
|
||||
func Degrees_to_radians(degrees float32) float32 {
|
||||
return (degrees * pi) / 180.0
|
||||
func Degrees_to_radians(degrees float32) float64 {
|
||||
return float64((degrees * pi) / 180.0)
|
||||
}
|
||||
|
||||
func Linear_to_gamma(linear_component float32) float32 {
|
||||
|
@ -336,6 +347,15 @@ func (s Sphere) Hit_sphere(r *Ray, ray_t *Interval, rec *Hit_record) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func NewSphere(center Vec3, radius float32, mat Material) Sphere {
|
||||
return Sphere{
|
||||
center: center,
|
||||
radius: radius,
|
||||
mat: mat,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============== HITTABLE ==================
|
||||
type Hittable struct {
|
||||
spheres []Sphere
|
||||
|
@ -395,6 +415,13 @@ func NewMaterial(m int, a Vec3, f float32, i float32) Material {
|
|||
|
||||
}
|
||||
|
||||
func Reflectance(cosine float32, ref_idx float32) float32 {
|
||||
// Use Schlick's approximation for reflectance
|
||||
r0 := (1.0-ref_idx) / (1.0+ref_idx)
|
||||
r0 = r0*r0
|
||||
return r0 + (1.0-r0)*float32(math.Pow(1.0 - float64(cosine), 5))
|
||||
}
|
||||
|
||||
func (mat Material) Scatter(r_in *Ray, rec *Hit_record, attenuation *Vec3, scattered *Ray) bool {
|
||||
if (mat.material == 0) { //Lambertian
|
||||
scatter_direction := rec.normal.Add(RandomUnitVector())
|
||||
|
@ -414,15 +441,26 @@ func (mat Material) Scatter(r_in *Ray, rec *Hit_record, attenuation *Vec3, scatt
|
|||
}
|
||||
} else if (mat.material == 2) { //Dielectric
|
||||
*attenuation = NewColor(1.0, 1.0, 1.0)
|
||||
var refraction_ration float32
|
||||
var refraction_ration float32 = 1.0
|
||||
if rec.front_face {
|
||||
refraction_ration = 1/mat.ir
|
||||
refraction_ration /= mat.ir
|
||||
} else {
|
||||
refraction_ration = mat.ir
|
||||
}
|
||||
unit_direction := Unit_vector(r_in.Direction())
|
||||
refracted := Refract(unit_direction, rec.normal, refraction_ration)
|
||||
*scattered = *NewRay(rec.p, refracted)
|
||||
|
||||
cos_theta := float32(math.Min(float64(Dot(unit_direction.Neg(), rec.normal)), 1.0))
|
||||
sin_theta := float32(math.Sqrt(float64(1.0 - cos_theta*cos_theta)))
|
||||
var direction Vec3
|
||||
cannot_refract := refraction_ration*sin_theta > 1.0
|
||||
|
||||
if (cannot_refract || Reflectance(cos_theta, refraction_ration) > RandomDouble()) {
|
||||
direction = Reflect(unit_direction, rec.normal)
|
||||
} else {
|
||||
direction = Refract(unit_direction, rec.normal, refraction_ration)
|
||||
}
|
||||
|
||||
*scattered = *NewRay(rec.p, direction)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -440,6 +478,17 @@ type Camera struct {
|
|||
pixel_delta_v Vec3
|
||||
samples_per_pixel int
|
||||
max_depth int
|
||||
vfov float32
|
||||
lookfrom Vec3
|
||||
lookat Vec3
|
||||
vup Vec3
|
||||
u Vec3
|
||||
v Vec3
|
||||
w Vec3
|
||||
defocus_angle float32
|
||||
focus_dist float32
|
||||
defocus_disk_u Vec3
|
||||
defocus_disk_v Vec3
|
||||
}
|
||||
|
||||
func NewCamera() *Camera {
|
||||
|
@ -471,23 +520,36 @@ func (cam *Camera) Initialize() {
|
|||
if cam.image_height < 1 {
|
||||
cam.image_height = 1
|
||||
}
|
||||
cam.center = cam.lookfrom
|
||||
|
||||
|
||||
// Viewport
|
||||
var focal_length float32 = 1.0
|
||||
var viewport_height float32 = 2.0
|
||||
theta := Degrees_to_radians(cam.vfov)
|
||||
h := float32(math.Tan(theta/2.0))
|
||||
var viewport_height float32 = 2.0 * h * cam.focus_dist
|
||||
var viewport_width float32 = viewport_height * float32(cam.image_width)/float32(cam.image_height)
|
||||
cam.center = NewVec3(0,0,0)
|
||||
|
||||
// Calculate the u,v,w unit basis vectors for the camera coordinate frame
|
||||
cam.w = Unit_vector(cam.lookfrom.Sub(cam.lookat))
|
||||
cam.u = Unit_vector(Cross(cam.vup, cam.w))
|
||||
cam.v = Cross(cam.w, cam.u)
|
||||
|
||||
// Calculate the vectors across the horizontal and down the vertical viewport edges
|
||||
viewport_u := NewVec3(viewport_width, 0, 0)
|
||||
viewport_v := NewVec3(0, -viewport_height, 0)
|
||||
viewport_u := cam.u.Mult(viewport_width)
|
||||
viewport_v := cam.v.Mult(-viewport_height)
|
||||
|
||||
//Calculate the horizontal and vertical delta vectors from pixel to pixel
|
||||
cam.pixel_delta_u = viewport_u.Div(float32(cam.image_width))
|
||||
cam.pixel_delta_v = viewport_v.Div(float32(cam.image_height))
|
||||
|
||||
// Calculate the location of the upper left pixel
|
||||
viewport_upper_left := cam.center.Sub(cam.center).Sub(NewVec3(0, 0, focal_length)).Sub(viewport_u.Div(2)).Sub(viewport_v.Div(2))
|
||||
viewport_upper_left := cam.center.Sub(cam.w.Mult(cam.focus_dist)).Sub(viewport_u.Div(2)).Sub(viewport_v.Div(2))
|
||||
cam.pixel00_loc = viewport_upper_left.Add((cam.pixel_delta_u.Add(cam.pixel_delta_v)).Div(2))
|
||||
|
||||
// Calculate the camera defocus disk basis vectors
|
||||
defocus_radius := cam.focus_dist*float32(math.Tan(Degrees_to_radians(cam.defocus_angle / 2)))
|
||||
cam.defocus_disk_u = cam.u.Mult(defocus_radius)
|
||||
cam.defocus_disk_v = cam.v.Mult(defocus_radius)
|
||||
}
|
||||
|
||||
func (cam *Camera) Render(world *Hittable) {
|
||||
|
@ -509,13 +571,26 @@ func (cam *Camera) Render(world *Hittable) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func (cam *Camera) Defocus_disk_sample() Vec3 {
|
||||
// Returns a random point in the camera defocus disk
|
||||
p := Random_in_unit_disk()
|
||||
return cam.center.Add(cam.defocus_disk_u.Mult(p.E[0])).Add(cam.defocus_disk_v.Mult(p.E[1]))
|
||||
}
|
||||
|
||||
func (cam *Camera) GetRay(i, j int) *Ray {
|
||||
// Get a randomly sampled camera ray for the pixel at location i,j
|
||||
// Get a randomly-sampled camera ray for the pixel at location i,j originating from the camera defocus disk
|
||||
pixel_center := cam.pixel00_loc.Add(cam.pixel_delta_u.Mult(float32(i))).Add(cam.pixel_delta_v.Mult(float32(j)))
|
||||
pixel_sample := pixel_center.Add(cam.Pixel_sample_square())
|
||||
|
||||
ray_direction := pixel_sample.Sub(cam.center)
|
||||
return NewRay(cam.center, ray_direction)
|
||||
var ray_origin Vec3
|
||||
if cam.defocus_angle <= 0 {
|
||||
ray_origin = cam.center
|
||||
} else {
|
||||
ray_origin = cam.Defocus_disk_sample()
|
||||
}
|
||||
ray_direction := pixel_sample.Sub(ray_origin)
|
||||
return NewRay(ray_origin , ray_direction)
|
||||
}
|
||||
|
||||
func (cam *Camera) Pixel_sample_square() Vec3 {
|
||||
|
@ -526,47 +601,79 @@ func (cam *Camera) Pixel_sample_square() Vec3 {
|
|||
}
|
||||
|
||||
|
||||
|
||||
// =============== MAIN =====================
|
||||
func main() {
|
||||
// Materials
|
||||
material_ground := NewMaterial(0, NewColor(0.8, 0.8, 0.0), 0.0, 0.0)
|
||||
material_center := NewMaterial(2, NewColor(0.7, 0.3, 0.3), 0.0, 1.5)
|
||||
material_left := NewMaterial(2, NewColor(0.8, 0.8, 0.8), 0.3, 1.5)
|
||||
material_right := NewMaterial(1, NewColor(0.8, 0.6, 0.2), 1.0, 0.0)
|
||||
|
||||
// World
|
||||
|
||||
world := NewHittable()
|
||||
|
||||
groundMaterial := NewMaterial(0, NewColor(0.5, 0.5, 0.5), 0.0, 0.0)
|
||||
world.Add(Sphere{
|
||||
center: NewVec3(0, -100.5, -1),
|
||||
radius: 100,
|
||||
mat: material_ground,
|
||||
center: NewVec3(0, -1000, 0),
|
||||
radius: 1000,
|
||||
mat: groundMaterial,
|
||||
})
|
||||
|
||||
for a := -11; a < 11; a++ {
|
||||
for b := -11; b < 11; b++ {
|
||||
chooseMat := RandomDouble()
|
||||
center := NewVec3(float32(a)+0.9*RandomDouble(), 0.2, float32(b)+0.9*RandomDouble())
|
||||
|
||||
if (center.Sub(NewVec3(4, 0.2, 0)).Length() > 0.9) {
|
||||
var sphereMaterial Material
|
||||
|
||||
if chooseMat < 0.8 {
|
||||
// diffuse
|
||||
albedo := RandomVec3().MultVec(RandomVec3())
|
||||
sphereMaterial = NewMaterial(0, albedo, 0.0, 0.0)
|
||||
} else if chooseMat < 0.95 {
|
||||
// metal
|
||||
albedo := RandomVec3(0.5, 1)
|
||||
fuzz := RandomDoubleInRange(0, 0.5)
|
||||
sphereMaterial = NewMaterial(1, albedo, fuzz, 0.0)
|
||||
} else {
|
||||
// glass
|
||||
sphereMaterial = NewMaterial(2, NewColor(1.0, 1.0, 1.0), 0.0, 1.5)
|
||||
}
|
||||
world.Add(Sphere{
|
||||
center: center,
|
||||
radius: 0.2,
|
||||
mat: sphereMaterial,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
material1 := NewMaterial(2, NewColor(1.0, 1.0, 1.0), 0.0, 1.5)
|
||||
world.Add(Sphere{
|
||||
center: NewVec3(0, 0, -1.0),
|
||||
radius: 0.5,
|
||||
mat: material_center,
|
||||
center: NewVec3(0, 1, 0),
|
||||
radius: 1.0,
|
||||
mat: material1,
|
||||
})
|
||||
|
||||
material2 := NewMaterial(0, NewColor(0.4, 0.2, 0.1), 0.0, 0.0)
|
||||
world.Add(Sphere{
|
||||
center: NewVec3(-1.0, 0, -1.0),
|
||||
radius: 0.5,
|
||||
mat: material_left,
|
||||
center: NewVec3(-4, 1, 0),
|
||||
radius: 1.0,
|
||||
mat: material2,
|
||||
})
|
||||
|
||||
material3 := NewMaterial(1, NewColor(0.7, 0.6, 0.5), 0.0, 0.0)
|
||||
world.Add(Sphere{
|
||||
center: NewVec3(1.0, 0, -1.0),
|
||||
radius: 0.5,
|
||||
mat: material_right,
|
||||
center: NewVec3(4, 1, 0),
|
||||
radius: 1.0,
|
||||
mat: material3,
|
||||
})
|
||||
// Image
|
||||
|
||||
cam := NewCamera()
|
||||
cam.aspect_ratio = 16.0 / 9.0
|
||||
cam.image_width = 400
|
||||
cam.samples_per_pixel = 100
|
||||
cam.max_depth = 50
|
||||
cam.image_width = 1200
|
||||
cam.samples_per_pixel = 200
|
||||
cam.max_depth = 45
|
||||
cam.vfov = 20
|
||||
cam.lookfrom = NewVec3(13, 2, 3)
|
||||
cam.lookat = NewVec3(0, 0, 0)
|
||||
cam.vup = NewVec3(0, 1, 0)
|
||||
cam.defocus_angle = 0.6
|
||||
cam.focus_dist = 10.0
|
||||
|
||||
cam.Render(world)
|
||||
// INFO: The pixels are written out in rows.
|
||||
// Image file can be created with
|
||||
// go run main.go > image.ppm
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue