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.