Page 1 of 2
ray tracing tutorial
Posted: Fri Feb 23, 2018 4:48 pm
by Hades
Hi Guys,
I've played with the idea of posting a tutorial for ray tracing for many years now, but wasn't quite happy with what I was coming up with. Explaining stuff and neat, simple coding aren't my strong suit...
But whatever, I will just give it a try.
I will focus on making the individual programs as simple and easy to understand as possible. The downside of this is, that to add more functionality, I will have to change a lot of code between programs.
So, please, use this just as a means to understand ray tracing, not as a reference how to best implement it.
Code: Select all
ScreenWidth = 600 : ScreenHeight = 400
If OpenWindow(0, 0, 0, ScreenWidth, ScreenHeight, "Ray Tracing Tutorial 1", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
If CreateImage(0, ScreenWidth, ScreenHeight)
If StartDrawing(ImageOutput(0))
ScreenZ = ScreenWidth ; Distance between the (virtual) eye and screen = width of screen for a 45 degrees horizontal field of view
; A sphere straight ahead and as high as the screen, but slightly further away
SphereRadius.f = ScreenHeight / 2
SphereX.f = 0
SphereY.f = 0
SphereZ.f = ScreenWidth * 1.2
; For every pixel on the screen
For ScreenY = 0 To ScreenHeight - 1
For ScreenX = 0 To ScreenWidth - 1
; Create a ray from the eye to the screen.
RayOriginX.f = 0 ; Position
RayOriginY.f = 0 ; of the
RayOriginZ.f = 0 ; eye
RayVecX.f = ScreenX - ScreenWidth / 2 ; Shift 0,0 to the center of the screen
RayVecY.f = ScreenHeight / 2 - ScreenY ; and making sure that up is positive
RayVecZ.f = ScreenZ
; Rays have a starting point and a direction (vector). When doing math with vectors the result will
; be in multiples of the length of the vector, so we need to make sure the lenght is 1, or it gets messy.
; This is called normalizing:
RayLenght.f = Sqr(RayVecX * RayVecX + RayVecY * RayVecY + RayVecZ * RayVecZ)
RayVecX = RayVecX / RayLenght
RayVecY = RayVecY / RayLenght
RayVecZ = RayVecZ / RayLenght
; Let's check if the ray hits the sphere.
RelPosX.f = RayOriginX - SphereX
RelPosY.f = RayOriginY - SphereY
RelPosZ.f = RayOriginZ - SphereZ
b.f = RayVecX * RelPosX + RayVecY * RelPosY + RayVecZ * RelPosZ
c.f = (RelPosX * RelPosX + RelPosY * RelPosY + RelPosZ * RelPosZ) - SphereRadius * SphereRadius
d.f = b * b - c
If d > 0.0 ; Ray vector intersects sphere. (Not testing if the sphere is in front or behind the ray origin for now)
Plot(ScreenX, ScreenY, RGB(200, 0 ,0))
EndIf
Next
Next
StopDrawing()
EndIf
EndIf
ImageGadget(0, 0, 0, 0, 0, ImageID(0))
Repeat
Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
EndIf
End
Re: ray tracing tutorial
Posted: Fri Feb 23, 2018 4:53 pm
by djes
Great news ! You have a follower

Re: ray tracing tutorial
Posted: Fri Feb 23, 2018 7:05 pm
by applePi
Thanks, can be useful for me since i was thinking to use ray tracing to know the size of a mesh without resorting to engine functions.
the small tutorials is the best to convey the ideas
Re: ray tracing tutorial
Posted: Sat Feb 24, 2018 7:39 am
by Hades
The result from the first code looked more like a filled circle than a sphere, and we actually shouldn't have been able to see it, because there was no light. Let's change that!
Code: Select all
ScreenWidth = 600 : ScreenHeight = 400
; Because we will use vectors and coordinates more and more, lets create a structure for it
Structure xyz
x.f
y.f
z.f
EndStructure
; Also a procedure for normalizing vectors will be useful
Procedure.f Normalize(*Vector.xyz)
Protected VectorLength.f
VectorLength = Sqr(*Vector\x * *Vector\x + *Vector\y * *Vector\y + *Vector\z * *Vector\z)
*Vector\x = *Vector\x / VectorLength
*Vector\y = *Vector\y / VectorLength
*Vector\z = *Vector\z / VectorLength
EndProcedure
If OpenWindow(0, 0, 0, ScreenWidth, ScreenHeight, "Ray Tracing Tutorial 2: Basic Light", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
If CreateImage(0, ScreenWidth, ScreenHeight)
If StartDrawing(ImageOutput(0))
ScreenZ = ScreenWidth
Define Sphere.xyz
SphereRadius.f = ScreenHeight / 2
Sphere\x = 0
Sphere\y = 0
Sphere\z = ScreenWidth * 1.2
; Create a directional light (like the sun, for all intents and purposes infinitly far away, with parallel light beams)
Define LightVec.xyz
LightVec\x = 1.0 ; from the right,
LightVec\y = 1.0 ; above,
LightVec\z = -1.0 ; and behind the eye
; and always normalize the vectors:
Normalize(LightVec)
For ScreenY = 0 To ScreenHeight - 1
For ScreenX = 0 To ScreenWidth - 1
Define RayOrigin.xyz, RayVec.xyz
RayOrigin\x = 0
RayOrigin\y = 0
RayOrigin\z = 0
RayVec\x = ScreenX - ScreenWidth / 2
RayVec\y = ScreenHeight / 2 - ScreenY
RayVec\z = ScreenZ
Normalize(RayVec)
Define RelPos.xyz
RelPos\x = RayOrigin\x - Sphere\x
RelPos\y = RayOrigin\y - Sphere\y
RelPos\z = RayOrigin\z - Sphere\z
b.f = RayVec\x * RelPos\x + RayVec\y * RelPos\y + RayVec\z * RelPos\z
c.f = (RelPos\x * RelPos\x + RelPos\y * RelPos\y + RelPos\z * RelPos\z) - SphereRadius * SphereRadius
d.f = b * b - c
If d > 0.0
; As we will now need the distance to the intersection anyway, I'll add the rest of the intersection test
d = Sqr(d)
DistanceToIntersection.f = -b - d
If DistanceToIntersection > 0.0 ; The intersection is in front of the ray origin. (Your not looking away from it)
; To figure out how much light hits the surface we need to determin the angle between the incoming light
; and the normal (perpendicular vector) of the surface at the point of intersection.
; For point of intersection we go from the origin of the ray along it's direction for the distance the intersection test gave us
Define Intersection.xyz
Intersection\x = RayOrigin\x + RayVec\x * DistanceToIntersection
Intersection\y = RayOrigin\y + RayVec\y * DistanceToIntersection
Intersection\z = RayOrigin\z + RayVec\z * DistanceToIntersection
; The normal of a point on a sphere has the same direction as a vector from the center of the sphere to that point on its surface.
Define SurfaceNormal.xyz
SurfaceNormal\x = Intersection\x - Sphere\x
SurfaceNormal\y = Intersection\y - Sphere\y
SurfaceNormal\z = Intersection\z - Sphere\z
Normalize(SurfaceNormal)
; The brightness of an ideal diffusely reflecting surface is directly proportional to the cosine of
; the angle between the surfaces normal and the incident light. (check Lambert's cosine law)
; And the 'dot product' (of two normalized vectors) gives us just that:
Brightness.f = SurfaceNormal\x * LightVec\x + SurfaceNormal\y * LightVec\y + SurfaceNormal\z * LightVec\z
If Brightness > 0.0
Plot(ScreenX, ScreenY, RGB(Brightness * 200, 0 ,0))
EndIf
EndIf
EndIf
Next
Next
StopDrawing()
EndIf
EndIf
ImageGadget(0, 0, 0, 0, 0, ImageID(0))
Repeat
Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
EndIf
End
Re: ray tracing tutorial
Posted: Sun Feb 25, 2018 8:37 am
by Hades
With light we need shadow too...
(I am trying to keep the comments in the code down, to not wear you down with walls of text stating the obvious, but figuring out what is obvious and what just seems that way to me, because I am doing this for so long, is close to impossible for me. So, if there are questions and complaints, please don't hold back)
Code: Select all
ScreenWidth = 600 : ScreenHeight = 400
Structure xyz
x.f
y.f
z.f
EndStructure
Structure sphere
radius.f
x.f
y.f
z.f
EndStructure
Global NewList Sphere.sphere()
Procedure Normalize(*Vector.xyz)
Protected VectorLength.f
VectorLength = Sqr(*Vector\x * *Vector\x + *Vector\y * *Vector\y + *Vector\z * *Vector\z)
*Vector\x = *Vector\x / VectorLength
*Vector\y = *Vector\y / VectorLength
*Vector\z = *Vector\z / VectorLength
EndProcedure
Procedure.f IntersectSphere(*RayOrigin.xyz, *RayVec.xyz, *Sphere.sphere)
Protected RelPos.xyz, b.f, c.f, d.f, DistanceToIntersection.f
RelPos\x = *RayOrigin\x - *Sphere\x
RelPos\y = *RayOrigin\y - *Sphere\y
RelPos\z = *RayOrigin\z - *Sphere\z
b = *RayVec\x * RelPos\x + *RayVec\y * RelPos\y + *RayVec\z * RelPos\z
c = (RelPos\x * RelPos\x + RelPos\y * RelPos\y + RelPos\z * RelPos\z) - *Sphere\radius * *Sphere\radius
d = b * b - c
If d > 0.0
d = Sqr(d)
DistanceToIntersection = -b - d
If DistanceToIntersection > 0.0
ProcedureReturn DistanceToIntersection
EndIf
EndIf
ProcedureReturn -1.0 ; no hit
EndProcedure
Procedure AddSphere(radius.f, x.f, y.f, z.f)
AddElement(Sphere())
Sphere()\radius = radius
Sphere()\x = x
Sphere()\y = y
Sphere()\z = z
EndProcedure
If OpenWindow(0, 0, 0, ScreenWidth, ScreenHeight, "Ray Tracing Tutorial 3: Shadow", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
If CreateImage(0, ScreenWidth, ScreenHeight)
If StartDrawing(ImageOutput(0))
ScreenZ = ScreenWidth
AddSphere(ScreenHeight / 2, 0, 0, ScreenWidth * 1.2) ; Well, I had thought that using multiples of the screen size for setting up
AddSphere(ScreenHeight * 50, 0, -ScreenHeight * 50.5, ScreenWidth * 1.2) ; the scene would help you with getting a feel for what's going on with the math.
AddSphere(ScreenHeight / 10, -ScreenHeight / 4, ScreenHeight / 4, ScreenWidth * 1.2 - ScreenHeight / 2) ; Somehow I start to doubt that now...
Define LightVec.xyz
LightVec\x = 1.0
LightVec\y = 1.0
LightVec\z = -1.0
Normalize(LightVec)
For ScreenY = 0 To ScreenHeight - 1
For ScreenX = 0 To ScreenWidth - 1
Define RayOrigin.xyz, RayVec.xyz
RayOrigin\x = 0
RayOrigin\y = 0
RayOrigin\z = 0
RayVec\x = ScreenX - ScreenWidth / 2
RayVec\y = ScreenHeight / 2 - ScreenY
RayVec\z = ScreenZ
Normalize(RayVec)
; Check all spheres for intersection
ClosestIntersection.f = 1000000.0 ; Arbitrary high value outside of the scene. (Can also be thought of as the lenght of the ray)
SphereHit = -1
ForEach Sphere()
DistanceToIntersection.f = IntersectSphere(RayOrigin, RayVec, Sphere())
; We are only interested in the closest hit...
If DistanceToIntersection > 0.0 And DistanceToIntersection < ClosestIntersection
ClosestIntersection = DistanceToIntersection ; so we need to keep track of that,
SphereHit = ListIndex(Sphere()) ; and of the sphere that was hit.
EndIf
Next
If SphereHit >= 0
SelectElement(Sphere(), SphereHit)
Define Intersection.xyz
Intersection\x = RayOrigin\x + RayVec\x * ClosestIntersection
Intersection\y = RayOrigin\y + RayVec\y * ClosestIntersection
Intersection\z = RayOrigin\z + RayVec\z * ClosestIntersection
Define SurfaceNormal.xyz
SurfaceNormal\x = Intersection\x - Sphere()\x
SurfaceNormal\y = Intersection\y - Sphere()\y
SurfaceNormal\z = Intersection\z - Sphere()\z
Normalize(SurfaceNormal)
Brightness.f = SurfaceNormal\x * LightVec\x + SurfaceNormal\y * LightVec\y + SurfaceNormal\z * LightVec\z
If Brightness > 0.0
; Let's check if there is something blocking the light.
Shadow = #False
ForEach Sphere()
; The sphere can't cast a shadow on itself (but due to rounding errors it might), so let's skip it.
If ListIndex(Sphere()) <> SphereHit
; To check for shadow we use the point of intersection as a new ray origin and the vector to the light as the ray vector...
If IntersectSphere(Intersection, LightVec, Sphere()) > 0.0 ; and if that ray hits something...
Shadow = #True ; we have shadow...
Break ; and we can stop looking any further.
EndIf
EndIf
Next
If Not Shadow
Plot(ScreenX, ScreenY, RGB(Brightness * 200, 0 ,0))
EndIf
EndIf
EndIf
Next
Next
StopDrawing()
EndIf
EndIf
ImageGadget(0, 0, 0, 0, 0, ImageID(0))
Repeat
Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
EndIf
End
Re: ray tracing tutorial
Posted: Sun Feb 25, 2018 12:16 pm
by #NULL
awesome, i love it
I didn't know you can do that with such little code.
Re: ray tracing tutorial
Posted: Sun Feb 25, 2018 6:53 pm
by djes
Great !

Re: ray tracing tutorial
Posted: Sun Feb 25, 2018 9:11 pm
by Hades
I am really happy you guys like it so far, because I was rather unsure about it.
I've taken a day off today, as I'm not feeling to well, so nothing for tomorrow.

Re: ray tracing tutorial
Posted: Sun Feb 25, 2018 10:43 pm
by kenmo
Take your time
I've played with ray-tracing in PureBasic, basically a few geometries with textures + bump-mapping... fun stuff.
I like these short, incremental demos too
Re: ray tracing tutorial
Posted: Tue Feb 27, 2018 9:48 am
by Kurzer
Hi Hades, thanks for your great tutorial.
Your code is easy to understand and clean, so that even someone without "raytracing experience" can follow all the steps. Really exciting to see how it works - also mathematically speaking. Since I never had to deal with complicated mathematics in my life, I have forgotten many things. All the more beautiful to see how elegantly it can actually be implemented.
This reminds me of my first contact with raytracing in the 80's on my old Amiga computer (the program was called Fastray, was extremely slow and - I think - programmed in pascal by an individual (a computer freak). I was fascinated.... today Fastray has become Cinema4D).
Thank you for this trip back in time.
Re: ray tracing tutorial
Posted: Tue Feb 27, 2018 10:42 am
by pf shadoko
very interesting
PS: Yes, you look bad indeed. You have a greyish tint and a yellow eye.
Re: ray tracing tutorial
Posted: Tue Feb 27, 2018 10:45 am
by DK_PETER
Just a note:
You should've reserved a number of posts at the beginning for your tuturial.
Anyway: Please, keep going, I like your tutorial.

Re: ray tracing tutorial
Posted: Tue Feb 27, 2018 1:30 pm
by Hades
Thanks for the nice words guys!
@kurzer: I've started ray tracing on the Amiga in the 80's too, but was more interested in coding one, as I lack any artistic skills.
@pf shadoko: That's me on a good day, you should see a current picture.
@DK_Peter: I thought about that, but I didn't know how much I would need, so I'll just put links in the first post, when it becomes necessary.
It takes me crazy long to write a single one of those programs, because I want to make them as clear as possible + my health sucks.
So my original plan of getting out 1 per day (what I thought was very slow), won't work.
I'll just see what I can get done, without any schedule.
Re: ray tracing tutorial
Posted: Tue Feb 27, 2018 8:50 pm
by Hades
And now reflections.
The lack of specular highlight and ambient light are rather obvious, but the usual way to do this for simple ray tracers are actually cheats and doing the real deal would probably to much for this tutorial series.
I'll hopefully squeeze in a part with a bunch of those cheats at some point, but I wanted to get the most important 'real' stuff done first.
Code: Select all
ScreenWidth = 600 : ScreenHeight = 400
Structure xyz
x.f
y.f
z.f
EndStructure
Structure sphere
radius.f
x.f
y.f
z.f
EndStructure
Global NewList Sphere.sphere()
Global LightVec.xyz
Procedure Normalize(*Vector.xyz)
Protected VectorLength.f
VectorLength = Sqr(*Vector\x * *Vector\x + *Vector\y * *Vector\y + *Vector\z * *Vector\z)
*Vector\x = *Vector\x / VectorLength
*Vector\y = *Vector\y / VectorLength
*Vector\z = *Vector\z / VectorLength
EndProcedure
Procedure.f IntersectSphere(*RayOrigin.xyz, *RayVec.xyz, *Sphere.sphere)
Protected RelPos.xyz, b.f, c.f, d.f, DistanceToIntersection.f
RelPos\x = *RayOrigin\x - *Sphere\x
RelPos\y = *RayOrigin\y - *Sphere\y
RelPos\z = *RayOrigin\z - *Sphere\z
b = *RayVec\x * RelPos\x + *RayVec\y * RelPos\y + *RayVec\z * RelPos\z
c = (RelPos\x * RelPos\x + RelPos\y * RelPos\y + RelPos\z * RelPos\z) - *Sphere\radius * *Sphere\radius
d = b * b - c
If d > 0.0
d = Sqr(d)
DistanceToIntersection = -b - d
If DistanceToIntersection > 0.0
ProcedureReturn DistanceToIntersection
EndIf
EndIf
ProcedureReturn -1.0 ; no hit
EndProcedure
Procedure AddSphere(radius.f, x.f, y.f, z.f)
AddElement(Sphere())
Sphere()\radius = radius
Sphere()\x = x
Sphere()\y = y
Sphere()\z = z
EndProcedure
; As with reflection, a ray hitting something can spawn one or more new rays that need to be traced through the scene, too.
; Doing this with recursion is the easiest way, so let's put everything into a procedure that can call itself.
Procedure.f TraceRay(*RayOrigin.xyz, *RayVec.xyz, RecursionDepth.i)
Protected ClosestIntersection.f, SphereHit.i, Brightness.f, A.f, Intersection.xyz, SurfaceNormal.xyz, ReflectedRayVec.xyz
ClosestIntersection = 1000000.0
SphereHit = -1
ForEach Sphere()
DistanceToIntersection.f = IntersectSphere(*RayOrigin, *RayVec, Sphere())
If DistanceToIntersection > 0.0 And DistanceToIntersection < ClosestIntersection
ClosestIntersection = DistanceToIntersection
SphereHit = ListIndex(Sphere())
EndIf
Next
If SphereHit >= 0
SelectElement(Sphere(), SphereHit)
Intersection\x = *RayOrigin\x + *RayVec\x * ClosestIntersection
Intersection\y = *RayOrigin\y + *RayVec\y * ClosestIntersection
Intersection\z = *RayOrigin\z + *RayVec\z * ClosestIntersection
SurfaceNormal\x = Intersection\x - Sphere()\x
SurfaceNormal\y = Intersection\y - Sphere()\y
SurfaceNormal\z = Intersection\z - Sphere()\z
Normalize(SurfaceNormal)
Brightness = SurfaceNormal\x * LightVec\x + SurfaceNormal\y * LightVec\y + SurfaceNormal\z * LightVec\z
If Brightness > 0.0
ForEach Sphere()
If ListIndex(Sphere()) <> SphereHit
If IntersectSphere(Intersection, LightVec, Sphere()) > 0.0
Brightness = 0.0 ; Shadow
Break
EndIf
EndIf
Next
Else
Brightness = 0.0 ; We don't want 'negative brightness' (when light hits the surface from behind)
EndIf
; Let's put a checkerboard pattern on the bottom sphere to make this less visually boring (just a quick hack, can be ignored)
If SphereHit = 1 And ((Int((1000 + Intersection\x) * 0.01) + Int(1000 + Intersection\z * 0.01)) & 1)
Brightness * 0.4
EndIf
; Reflection:
If RecursionDepth < 10 ; To prevent stack overflow due to too many recursions (just in case you start experimenting with the code)
If SphereHit = 0 ; first sphere in list is reflective
; Compute the reflected ray by projecting the incoming ray onto the surface normal \| -> |/
A = 2.0 * (SurfaceNormal\x * *RayVec\x + SurfaceNormal\y * *RayVec\y + SurfaceNormal\z * *RayVec\z) ; 2 * dot product
ReflectedRayVec\x = *RayVec\x - A * SurfaceNormal\x
ReflectedRayVec\y = *RayVec\y - A * SurfaceNormal\y
ReflectedRayVec\z = *RayVec\z - A * SurfaceNormal\z
; trace the reflected ray from the point of intersection through the scene
Brightness * 0.3 + 0.7 * TraceRay(Intersection, ReflectedRayVec, RecursionDepth + 1) ; 30% diffuse + 70% reflective
EndIf
EndIf
EndIf
ProcedureReturn Brightness
EndProcedure
If OpenWindow(0, 0, 0, ScreenWidth, ScreenHeight, "Ray Tracing Tutorial 4: Reflection", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
If CreateImage(0, ScreenWidth, ScreenHeight)
If StartDrawing(ImageOutput(0))
ScreenZ = ScreenWidth
AddSphere(ScreenHeight / 2, 0, 0, ScreenWidth * 1.2)
AddSphere(ScreenHeight * 50, 0, -ScreenHeight * 50.5, ScreenWidth * 1.2)
AddSphere(ScreenHeight / 10, -ScreenHeight / 4, ScreenHeight / 4, ScreenWidth * 1.2 - ScreenHeight / 2)
LightVec\x = 1.0
LightVec\y = 1.0
LightVec\z = -1.0
Normalize(LightVec)
For ScreenY = 0 To ScreenHeight - 1
For ScreenX = 0 To ScreenWidth - 1
Define RayOrigin.xyz, RayVec.xyz
RayOrigin\x = 0
RayOrigin\y = 0
RayOrigin\z = 0
RayVec\x = ScreenX - ScreenWidth / 2
RayVec\y = ScreenHeight / 2 - ScreenY
RayVec\z = ScreenZ
Normalize(RayVec)
Brightness.f = TraceRay(RayOrigin, RayVec, 1)
Plot(ScreenX, ScreenY, RGB(Brightness * 200, 0 ,0))
Next
Next
StopDrawing()
EndIf
EndIf
ImageGadget(0, 0, 0, 0, 0, ImageID(0))
Repeat
Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
EndIf
End
Re: ray tracing tutorial
Posted: Fri Mar 02, 2018 1:06 pm
by pf shadoko
such a beautiful result in so little line...
you could use vector operators
like this one
Code: Select all
Macro vec3d(v,vx,vy,vz)
v\x=vx
v\y=vy
v\z=vz
EndMacro
Macro sub3D(p,p1,p2)
p\x=p1\x-p2\x
p\y=p1\y-p2\y
p\z=p1\z-p2\z
EndMacro
Macro add3d(p,p1,p2)
p\x=p1\x+p2\x
p\y=p1\y+p2\y
p\z=p1\z+p2\z
EndMacro
Macro div3d(p1,v)
p1\x/v
p1\y/v
p1\z/v
EndMacro
Macro mul3d(p1,v)
p1\x*v
p1\y*v
p1\z*v
EndMacro