This example uses OpenLayers v10.9.0. The latest is v.
Edit

#### Shaded Relief (with WebGL)

webgl 18 shadedrelief 2

#####

vertical exaggeration: |  | x
sun elevation: |  | °
sun azimuth: |  | °

Calculate shaded relief from elevation data

For the shaded relief, a single tiled source of elevation data is used as input. The shaded relief is calculated by the layer's `style` with a `color` expression. The style variables are updated when the user drags one of the sliders. The `band` operator is used to sample data from neighboring pixels for calculating slope and aspect, which is done with the `['band', bandIndex, xOffset, yOffset]` syntax.

##### main.js

    import Map from 'ol/Map.js';
    import View from 'ol/View.js';
    import TileLayer from 'ol/layer/WebGLTile.js';
    import ImageTile from 'ol/source/ImageTile.js';
    import OSM from 'ol/source/OSM.js';

    const variables = {};

    // The method used to extract elevations from the DEM.
    // In this case the format used is Terrarium
    // red * 256 + green + blue / 256 - 32768
    //
    // Other frequently used methods include the Mapbox format
    // (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000
    //
    function elevation(xOffset, yOffset) {
      const red = ['band', 1, xOffset, yOffset];
      const green = ['band', 2, xOffset, yOffset];
      const blue = ['band', 3, xOffset, yOffset];

      // band math operates on normalized values from 0-1
      // so we scale by 255
      return [
        '+',
        ['*', 255 * 256, red],
        ['*', 255, green],
        ['*', 255 / 256, blue],
        -32768,
      ];
    }

    // Generates a shaded relief image given elevation data.  Uses a 3x3
    // neighborhood for determining slope and aspect.
    const dp = ['*', 2, ['resolution']];
    const z0x = ['*', ['var', 'vert'], elevation(-1, 0)];
    const z1x = ['*', ['var', 'vert'], elevation(1, 0)];
    const dzdx = ['/', ['-', z1x, z0x], dp];
    const z0y = ['*', ['var', 'vert'], elevation(0, -1)];
    const z1y = ['*', ['var', 'vert'], elevation(0, 1)];
    const dzdy = ['/', ['-', z1y, z0y], dp];
    const slope = ['atan', ['sqrt', ['+', ['^', dzdx, 2], ['^', dzdy, 2]]]];
    const aspect = ['clamp', ['atan', ['-', 0, dzdx], dzdy], -Math.PI, Math.PI];
    const sunEl = ['*', Math.PI / 180, ['var', 'sunEl']];
    const sunAz = ['*', Math.PI / 180, ['var', 'sunAz']];

    const cosIncidence = [
      '+',
      ['*', ['sin', sunEl], ['cos', slope]],
      ['*', ['cos', sunEl], ['sin', slope], ['cos', ['-', sunAz, aspect]]],
    ];
    const scaled = ['*', 255, cosIncidence];

    const shadedRelief = new TileLayer({
      opacity: 0.3,
      source: new ImageTile({
        url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
        maxZoom: 15,
        attributions:
          '<a href="https://github.com/tilezen/joerd/blob/master/docs/attribution.md" target="_blank">Data sources and attribution</a>',
      }),
      style: {
        variables: variables,
        color: ['color', scaled],
      },
    });

    const controlIds = ['vert', 'sunEl', 'sunAz'];
    controlIds.forEach(function (id) {
      const control = document.getElementById(id);
      const output = document.getElementById(id + 'Out');
      function updateValues() {
        output.innerText = control.value;
        variables[id] = Number(control.value);
      }
      updateValues();
      control.addEventListener('input', function () {
        updateValues();
        shadedRelief.updateStyleVariables(variables);
      });
    });

    const map = new Map({
      target: 'map',
      layers: [
        new TileLayer({
          source: new OSM(),
        }),
        shadedRelief,
      ],
      view: new View({
        center: [-13615645, 4497969],
        zoom: 13,
      }),
    });

##### index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <title>Shaded Relief (with WebGL)</title>
        <link rel="stylesheet" href="node_modules/ol/ol.css">
        <style>
          .map {
            width: 100%;
            height: 400px;
          }
          table.controls td {
            padding: 2px 5px;
          }
          table.controls td:nth-child(3) {
            text-align: right;
            min-width: 3em;
          }
        </style>
      </head>
      <body>
        <div id="map" class="map"></div>
        <table class="controls">
          <tr>
            <td><label for="vert">vertical exaggeration:</label></td>
            <td><input id="vert" type="range" min="1" max="5" value="1"/></td>
            <td><span id="vertOut"></span> x</td>
          </tr>
          <tr>
            <td><label for="sunEl">sun elevation:</label></td>
            <td><input id="sunEl" type="range" min="0" max="90" value="45"/></td>
            <td><span id="sunElOut"></span> °</td>
          </tr>
          <tr>
            <td><label for="sunAz">sun azimuth:</label></td>
            <td><input id="sunAz" type="range" min="0" max="360" value="45"/></td>
            <td><span id="sunAzOut"></span> °</td>
          </tr>
        </table>

        <script type="module" src="main.js"></script>
      </body>
    </html>

##### package.json

    {
      "name": "webgl-shaded-relief",
      "dependencies": {
        "ol": "10.9.0"
      },
      "devDependencies": {
        "vite": "^3.2.3"
      },
      "scripts": {
        "start": "vite",
        "build": "vite build"
      }
    }