In this post I will introduce the concept of textures (and apply it to our plane-primitive) and explain why I choose to add a fog effect.
This is the texture we are going to tile to our “floor”:
What is a texture?
Well for us the answer is very straight-forward. It’s a bitmap we want to glue to some 3D object. For our (unbounded) plane we will extend this to cover the surface with a tiling of the same bitmap.
I choose to model a texture as a function taking two values (x and y – value on the bitmap ranging from 0 to 1) and returning the color of the texture at this point:
type Texture = { getColor : (float*float) -> Color }
And so we need a “gluing”-map that translates a point on the surface of our object (a hit-point) to a coordinate in the texture (remember, x and y between 0 and 1):
type TextureCoordinateMap = Point -> (float*float)
Change to our model
I toyed a bit with this and finally realized that I should change the models for our ray-tracer to include the texture theme. This is what I ended with (at this time):
type HitParams = { Color : Color; Specular : float; Reflection : float }
type HitResult = { Ray : Ray; Distance : float; Pos : Point; Normal : Direction; Params : HitParams }
type SceneObj = { HitTest : Ray -> HitResult option }
type Texture = { getColor : (float*float) -> Color }
type MaterialTexture =
| SolidColor of Color
| Textured of Texture
type MaterialParams = { Specular : float; Reflection : float }
type TextureCoordinateMap = Point -> (float*float)
type Material = { Texture : MaterialTexture; Params : MaterialParams } with
member m.GetHitParams (hitPoint : Point, map : TextureCoordinateMap) =
let makeHitParams color =
{ Color = color;
Specular = m.Params.Specular;
Reflection = m.Params.Reflection }
match m.Texture with
| SolidColor c -> makeHitParams c
| Textured t -> hitPoint |> map |> t.getColor |> makeHitParams
As you can see I refactored the Specular and Reflection parameters from the color and included two different models for the color – solid color where every point will have the same color and Textured where the color will be determined by the texture pasted to the object. In addition Material now got a method to calculate the HitParams (Color + Specular and Reflection) given a hit-point a “gluing”-map.
The changed this introduce to the primitives so far are strange forward (only the glue-map is interesting).
Coordinate map for a repeating tiling for the plane
First I had to change the creation of a plane to include not the normal but two directions on the plane (the normal is just the cross-product of those). I need this to determine in which direction we should glue the texture onto the plane – the base point on the plane will be the base-point for this gluing too.
The length of the given directions will give us a hint of how big the textures should be – they determine the width and height of the area where the texture is transformed into. The texture is just repeated in a grid-like way to fill the hole plane.
This is the code for the new plane:
module Plane =
let private createCoordinateMap (basePoint : Point, dirX : Direction, dirY : Direction) =
let modulo (x : float) =
let integer = System.Math.Floor(x)
x - integer
let l2X = dirX |> Vector.Len2
let l2Y = dirY |> Vector.Len2
fun (hitPoint : Point) ->
let pt = hitPoint - basePoint
let dx = (pt <*> dirX) / l2X |> modulo
let dy = (pt <*> dirY) / l2Y |> modulo
(dx, dy)
let private createNormalPlane (basePoint : Point, dirX : Direction, dirY : Direction) =
let dX = dirX |> Vector.Normalize
let dY = dirY |> Vector.Normalize
let n = dY <%> dX
(basePoint, n)
let private createNormalPlaneFromPoints (a : Point, b : Point, c : Point) =
let dirX = (b - a)
let dirY = (c - a)
createNormalPlane (a, dirX, dirY)
let Create (basePoint : Point, dirX : Direction, dirY : Direction, material : Material) : SceneObj =
let (a, n) = createNormalPlane (basePoint, dirX, dirY)
let coordMap = createCoordinateMap (basePoint, dirX, dirY)
let getPointOnRay (ray : Ray) (t : float) : HitResult option =
if t |> IsPositive then
let pos = ray.Start + t*ray.Direction
{ Ray = ray; Distance = t;
Pos = pos; Normal = n;
Params = material.GetHitParams(pos, coordMap) }
|> Some
else
None
let intersectRayWithPlane (ray : Ray) : float option =
let k = ray.Direction <*> n
match k with
| NearZero -> None
| _ -> let l = (a - ray.Start) <*> n
Some (l/k)
let hitTest ray = ray |> intersectRayWithPlane |> Option.bind (getPointOnRay ray)
{ HitTest = hitTest }
Why fog?
Well if I use the code I did show you so far without any bounding a “floor”-plane will be seen so far that numerical problems with floating-point numbers will result in some ugly-distortions on the floor:
Decide for yourself if this is an issue or not – I don’t like this so I introduced fog.
The idea is very simple – based on the distance from the eye we slowly blend the color to some back/fog-color:
// calculate fog
let fog (hitPoint : HitResult) =
if hitPoint.Distance < 9.0/10.0 * fogDist then 0.0 else
let fogS = (fogDist - hitPoint.Distance) / fogDist |> min 1.0
let fogF = fogS ** 4.0
fogF
// return the shaded color + reflection color
// use fog
let color hit =
let basic = shadeHit hit + reflectHit hit
let fogColor = Colors.Black
let fog = fog hit
(1.0 - fog) * basic + fog * fogColor
As you can see I choose to cap the effect on the last 10% of the view-distance (= fogDist) and raised it to the 4th power to decrease the effect a bit.
This will yield the following result (for a fog – distance of 100):
The coordinate-map for the sphere need some more math and I will talk on this in the next part of the series.