FunTracer: Textures and Fog

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”:

BlackAndWhite

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:

FunTracer texture without fog

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):

FunTracer with fog

The coordinate-map for the sphere need some more math and I will talk on this in the next part of the series.