Lightning
=========

The full source code for this part of the tutorial is available in the file
`root/tutorials/s3d/light.bs`. You can run it using `storm -f tutorials.s3d.light`. As in the
previous parts, the code is fairly well-documented, and is likely possible to read on its own.

This tutorial builds upon the code from the [model tutorial](md:Models). If you started writing your
own code previously, you can continue working on it. Note, however, that the names of types and
shaders have been modified from `Model...` to `Light...` to avoid name clashes in the tutorial
package.


Updating the Pipeline
---------------------

As before, we start by updating the pipeline. This time the changes are driven by the fact that the
pixel shader needs to have more information than before. In particular, we need to make the
following changes:

- Since we will perform lightning in world-space, we need to split the `camera` matrix into two
  pieces. We call one of them `world`, and let it translate from model to world space. We call the
  other one `camera` and let it transform vertices from world space to camera space.

  The reason for doing this is that it lets us retain points and normals in world space. It is more
  intuitive to perform lightning calculations in this coordinate system, as we don't have to
  counteract the effects of the projection that is at the end of the `camera` matrix.

- It turns out that we need a different matrix for transforming vertex normals (for example, we
  don't want to apply translations to normals). We need to store that matrix as well (`forNormal`).

- The pixel shader also needs to know the camera position in world-space (`cameraPos`), as well as
  the position of the light in world space (`lightPos`).

- Finally, we store information about global light properties in the pipeline.

All in all, we update the pipeline to look like below:

```bsfragment:SFile:use=ui.s3d
pipeline LightPipeline {
    data (Vertex vertex) -> LightIntermediate -> Output;

    // Model space to world space. Used to transform positions.
    Transform worldPosition;

    // Model space to world space. Used to transform normals.
    Transform worldNormal;

    // World space to camera space.
    Transform camera;

    // Camera position in world space.
    Vector cameraPos;

    // Position of the light in world space.
    Vector lightPos;

    // Ambient light strength.
    Float ambientStrength;

    // Specular light strength and exponent.
    Float specularStrength;
    Float specularExponent;

    // Specular color.
    Color specularColor;
}
```

We also need to update the intermediate vertex type. In particular, we need to include the
world-space position, and normal coordinates. This gives us the following:

```bsfragment:SFile:use=ui.s3d
vertex LightIntermediate {
    // Screen space position (as before).
    Vector4 pos : position, perspective;

    // World position. Used for lightning.
    Vector world : perspective;

    // Normal direction transformed to world space. For lightning.
    Vector normal : perspective;

    // Vertex color. Used for ambient and diffuse lightning.
    Color color : perspective;

    // Texture coordinates, if we want to apply textures.
    Point tex : perspective;
}
```

Finally, we need to update the shaders. As a starting point, we modify the vertex shader to provide
all relevant information to the pixel shader, but we wait to implement light calculations until a
bit later.

```bsfragment:SFile:use=ui.s3d
vertex shader for LightPipeline: LightIntermediate lightVertexShader(Vertex point) {
    Vector4 worldPos = Vector4(point.pos) * worldPosition;
    Vector4 screenPos = worldPos * camera;

    // Transform the normal. Note that we need to normalize it at some point. We do
    // that later, since the interpolation between the vertex and pixel shaders will
    // undo any normalization we perform here anyway.
    Vector normal = point.normal * worldNormal;

    LightIntermediate(screenPos, Vector(worldPos), normal, point.color, point.tex);
}
```

As you can see from above, the vertex shader resembles the ones we have seen so far. The main
difference this time is that we transform the model's coordinates in two steps. First, we apply the
`world` transform to compute its world space coordinates. We then apply `camera` to compute its
screen space coordinates. This allows us to save the world coordinates and give them to the pixel
shader to perform lighting.

For now, we use a simple pixel shader. That way we can get something on the screen to see that we
are supplying (approximately) the correct data to the rendering pipeline at least.

```bsfragment:SFile:use=ui.s3d
pixel shader for LightPipeline: Output lightPixelShader(LightIntermediate point) {
    Output(point.color);
}
```

Updating the Painter
--------------------

Before we can perform any lightning, we need to supply the new values to the pipeline. To do that,
we need some slight modifications to our painter.

We start by setting the global light properties in the constructor of the painter:

```bsfragment:SClassBody
init() {
    init {
        pipeline = LightPipeline:create(lightVertexShader, lightPixelShader);
        cameraPos = rotateX(-35 deg) * translate(Vector(0, 0, 3));
        model = resUrl("teapot.obj").loadOBJ.withColor(red);
    }

    // Set ambient light strength.
    pipeline.ambientStrength = 0.1;

    // Set specular light properties.
    pipeline.specularStrength = 0.8;
    pipeline.specularExponent = 32.0;
    pipeline.specularColor = white;
}
```

Then, perhaps the more interesting change. In the `render` function we need to compute and update
the transforms that the shaders use. We will also add code to make the light source rotate a bit
above the model so that we can observe the light from different angles more easily.

```bsfragment:SClassBody
Bool render(Size size, Graphics3D g) : override {
    Float time = animationTime(20 s);  // So that we can have different speeds!

    // Clear the old buffer.
    g.clearDepth();
    g.fill(black);

    // Set the world transforms.
    Transform world = rotateY(360 deg * time * 4) * scale(0.5);
    pipeline.worldPosition = world;
    pipeline.worldNormal = world.forNormal();

    // Set the camera transform.
    pipeline.camera = cameraPos * perspectiveProject(100 deg, size.aspect, 0.1, 100);

    // Set the light position. We let it rotate above the model in a big circle, but slower.
    pipeline.lightPos = Vector(0, 10, -10) * rotateY(360 deg * -time);

    // Compute the camera position, for specular highlights.
    pipeline.cameraPos = Vector(0, 0, 0) * cameraPos.inverted;

    // Now, we can render the model as before.
    pipeline.render(g, model);

    true;
}
```

It is worth noting that `forNormal` involves inverting the matrix. This is why we make sure to do
compute the matrix once on the CPU, rather than letting the shaders do the computation for each
pixel.

At this point you can run the program. Perhaps dissapointingly, you will just see the same rotating
teapot as before, since we have not yet used the new information available to the shaders.


Convenient Updates of Matrices
------------------------------

As you can see above, we need to remember to set multiple shader uniforms at the same time. We can
simplify this a bit by utilizing the fact that a pipeline is essentially a class. In particular,
this allows us to create assign functions that update multiple uniforms in one go. For example, to
update the world position conveniently, we can add an assign function as follows:

```bsfragment:SFile:use=ui.s3d
pipeline LightPipeline {
    data (Vertex vertex) -> LightIntermediate -> Output;

    // Model space to world space. Used to transform positions.
    Transform worldPosition;

    // Model space to world space. Used to transform normals.
    Transform worldNormal;

    // Set both 'worldPosition' and 'worldNormal' in one go.
    assign world(Transform tfm) {
        worldPosition = tfm;
        worldNormal = tfm.forNormal();
    }

    // ...
}
```

This lets us replace the two assignments to `worldPosition` and `worldNormal` in our `render`
function with:

```bsstmt
// Set the world transforms.
pipeline.world = rotateY(360 deg * time * 4) * scale(0.5);
```


Ambient Light
-------------

Now it is time to actually apply some lightning. In this tutorial we will implement what is
sometimes referred to as the Phong lightning model. We will start with what is perhaps the most
boring part, ambient light. This portion of light models the "stray" light that has bounced around
in the scene enough to have no direction in particular. As such, it is often modeled using only a
factor (or a dark color) to represent the fact that ambient light is typically rather faint. For
example, we just set the ambient light to 0.1 in the painter.

In the pixel shader, we can implement it as follows:

```bsfragment:SFile:use=ui.s3d
pixel shader for LightPipeline: Output lightPixelShader(LightIntermediate point) {
    // Ambient light:
    Color light = point.color * ambientStrength;

    Output(light);
}
```

As you can see, ambient light only scales the ordinary color of the vertex. As such, if we run the
program now, the only thing we will see is a much darker version of the teapot.


Diffuse Light
-------------

The second part of the light is *diffuse light*. This part of light models the fact that surfaces
that are perpendicular to a light source receives a light of higher intensity than surfaces that are
angled to the light. Or equivalently, the surface receives the most light if the surface normal
points directly at the light source. Note that this simplification assumes that once the light hits
the surface, the roughness of the surface reflects the light evenly in all directions. Because of
this, the camera position does not matter for diffuse light.

We can model it in the pixel shader as follows:

```bsfragment:SFile:use=ui.s3d
pixel shader for LightPipeline: Output lightPixelShader(LightIntermediate point) {
    // Compute directions to light sources and normalize the normal vector
    // (interpolation and translation may have altered its length).
    Vector lightDir = (lightPos - point.world).normalized;
    Vector normal = point.normal.normalized;

    // Ambient light:
    Color light = point.color * ambientStrength;

    // Diffuse light:
    Float diffuse = max(normal * lightDir, 0.0);
    light += point.color * diffuse;

    Output(light);
}
```

Note that we can simply compute the light strength as the dot product between the light direction
and the surface normal. Since both vectors are normalized, this gives us the cosine of the angle
between the two vectors, which corresponds to how light intensity falls off with angles. We then use
`max` to avoid negative light intensities which will mess with the other parts of lightning (e.g.
entirely eliminate the ambient light).

If we run the code at this point, we will get a more exciting result. The addition of ambient light
highlight the fact that the teapot is indeed not flat, but rather has some volume to it!


Specular Light
--------------

The final part of the Phon lightning model is *specular light*, or sometimes called *specular
highlights*. This part models the fact that most surfaces do not reflect light evenly in all
directions. Rather, they reflect more light in the direction directly perpendicular to the incoming
light. This makes objects appear shiny.

In our pixel shader, we can implement it as follows:

```bsfragment:SFile:use=ui.s3d
pixel shader for LightPipeline: Output lightPixelShader(LightIntermediate point) {
    // Compute directions to light sources, the camera, and normalize the normal vector
    // (interpolation and translation may have altered its length).
    Vector cameraDir = (cameraPos - point.world).normalized;
    Vector lightDir = (lightPos - point.world).normalized;
    Vector normal = point.normal.normalized;

    // Ambient light:
    Color light = point.color * ambientStrength;

    // Diffuse light:
    Float diffuse = max(normal * lightDir, 0.0);
    light += point.color * diffuse;

    // Specular light:
    Vector reflectDir = lightDir.reflect(normal);
    Float specular = pow(max(cameraDir * reflectDir, 0.0), specularExponent);
    light += specularColor * specularStrength * specular;

    Output(light);
}
```

Note that we use the `reflect` member of `Vector` to compute the reflection direction of the light
as it is reflected in a plane with a normal of `normal`. The computation for the strength of the
light is then quite similar to the diffuse light: we compute the intensity using the dot product and
make sure it is positive using `max`. However, to make the reflection more narrow, we raise the
result to 32. We then scale the output with the specular intensity and add the specular color to our
final light mix.

At this point we are finished. Running the example now will show some highlights which makes the
teapot look shiny.
