<!DOCTYPE html>

<html>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />

<body style="background-color: black;">
    <script>

        // Multiply two matrices, stored as length 16 arrays in column-major order.
        function matrix_mul(m1, m2) {
            let out = Array(16).fill(0);
            for (let i = 0; i < 4; i++) {
                for (let j = 0; j < 4; j++) {
                    for (let k = 0; k < 4; k++) {
                        out[4 * i + k] += m1[4 * j + k] * m2[4 * i + j];
                    }
                }
            }
            return out;
        }


        // MESH SETUP //

        // A triangle mesh.
        // Contains position and normal data.
        let mesh_data = Float32Array.from([
            /* pos */+0.176, +0.9, +0.8382, +0.8, /* norm */ 0.0, 0.236, 1.382, -1.618,
            /* pos */+0.176, +0.8, +0.9, +0.8382, /* norm */ 0.0, 0.236, 1.382, -1.618,
            /* pos */-0.176, +0.8, +0.9, +0.8382, /* norm */ 0.0, 0.236, 1.382, -1.618,
        ]);

        // 14400 transformation matrices.
        let instance_data = function () {
            function quaternion_mul(q1, q2) {
                let [w1, x1, y1, z1] = q1;
                let [w2, x2, y2, z2] = q2;
                return [w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
                    , w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
                    , w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
                    , w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
                ];
            }

            let binary_icosahedral_group = Array();
            let q = [0.809, 0.309, 0.5, 0];
            [
                [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1],
                [0.5, 0.5, 0.5, 0.5], [0.5, 0.5, 0.5, -0.5], [0.5, 0.5, -0.5, 0.5], [0.5, 0.5, -0.5, -0.5],
                [0.5, -0.5, 0.5, 0.5], [0.5, -0.5, 0.5, -0.5], [0.5, -0.5, -0.5, 0.5], [0.5, -0.5, -0.5, -0.5]
            ].forEach(tmp => {
                for (let i = 0; i < 10; i++) {
                    binary_icosahedral_group.push(tmp);
                    tmp = quaternion_mul(q, tmp);
                }
            });

            let matrices = Array();
            binary_icosahedral_group.forEach(q1 => binary_icosahedral_group.forEach(q2 =>
                [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]].forEach(v =>
                    quaternion_mul(q1, quaternion_mul(v, q2)).forEach(x => { matrices.push(x); })
                )
            ));

            return Float32Array.from(matrices);
        }();



        // GL //

        // Canvas width and height, in pixels.
        let canvas_size = 800;

        let canvas = window.document.createElement("canvas");
        canvas.setAttribute("width", canvas_size);
        canvas.setAttribute("height", canvas_size);
        canvas.setAttribute("style", "background-color: #555; cursor: pointer;");
        window.document.body.appendChild(canvas);

        canvas.addEventListener("click", function (e) { canvas.requestPointerLock(); });

        // Perspective transformation matrix 
        //   aspect ratio = 1
        //   fov = 90°
        //   znear = atan(0.1)
        //   zfar = π - atan(0.1)
        let perspective = [
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 0, -1,
            0, 0, -0.01, 0
        ];

        // The character's current position and orientation, as a matrix.
        let transform = [
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ];


        let gl = canvas.getContext("webgl2");

        gl.clearColor(0.0, 0.0, 0.0, 0.0);
        gl.viewport(0, 0, canvas_size, canvas_size);
        gl.enable(gl.DEPTH_TEST);

        let vshader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vshader,
            '#version 300 es\n' +
            'in vec4 pos;\n' +
            'in vec4 norm;\n' +
            'in mat4 instance_transform;\n' +
            'out vec4 vpos;\n' +
            'out vec4 vnorm;\n' +
            'out vec4 lightpos[3];\n' +
            'uniform mat4 transform;\n' +
            'uniform mat4 perspective;\n' +
            'uniform vec4 lightPositions[3];\n' +
            'void main() {\n' +
            '    vpos = transform * instance_transform * pos;\n' +
            '    vnorm = transform * instance_transform * norm;\n' +
            '    lightpos[0] = transform * lightPositions[0];\n' +
            '    lightpos[1] = transform * lightPositions[1];\n' +
            '    lightpos[2] = transform * lightPositions[2];\n' +
            '    gl_Position = perspective * vpos;\n' +
            '}\n'
        );
        gl.compileShader(vshader);

        let fshader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fshader,
            '#version 300 es\n' +
            'precision mediump float;\n' +
            'in vec4 vpos;\n' +
            'in vec4 vnorm;\n' +
            'in vec4 lightpos[3];\n' +
            'out vec4 color;\n' +

            // A vector in the tangent space of v, pointing toward u.
            'vec4 directionTo(vec4 u, vec4 v) {\n' +
            '    return u - v * (dot(u, v) / dot(v, v));\n' +
            '}\n' +

            // If there is a light at u, how much light reaches v?
            'float lighting(vec4 u, vec4 v) {\n' +
            '    float x = dot(normalize(u), normalize(v));\n' +
            '    return 1. / (1. - x * x);\n' +
            '}\n' +

            'void main() {\n' +
            '    color = vec4(0.3, 0.3, 0.3, 1.0);\n' +

            '    float cos_angle;\n' +

            '    cos_angle = dot(\n' +
            '        normalize(directionTo(lightpos[0], vpos)),\n' +
            '        normalize(vnorm)\n' +
            '    );\n' +
            '    color.x += 0.3 * max(0., cos_angle) * lighting(lightpos[0], vpos);\n' +

            '    cos_angle = dot(\n' +
            '        normalize(directionTo(lightpos[1], vpos)),\n' +
            '        normalize(vnorm)\n' +
            '    );\n' +
            '    color.y += 0.3 * max(0., cos_angle) * lighting(lightpos[1], vpos);\n' +

            '    cos_angle = dot(\n' +
            '        normalize(directionTo(lightpos[2], vpos)),\n' +
            '        normalize(vnorm)\n' +
            '    );\n' +
            '    color.z += 0.3 * max(0., cos_angle) * lighting(lightpos[2], vpos);\n' +
            '}\n'
        );
        gl.compileShader(fshader);

        let program = gl.createProgram();
        gl.attachShader(program, vshader);
        gl.attachShader(program, fshader);
        gl.linkProgram(program);
        gl.useProgram(program);

        gl.deleteShader(vshader);
        gl.deleteShader(fshader);




        let vao = gl.createVertexArray();
        gl.bindVertexArray(vao);

        let vertex_buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
        gl.bufferData(gl.ARRAY_BUFFER, mesh_data, gl.STATIC_DRAW);

        let instance_buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, instance_buffer);
        gl.bufferData(gl.ARRAY_BUFFER,
            instance_data,
            gl.STATIC_DRAW,
        );

        let attrib_pos = gl.getAttribLocation(program, "pos");
        let attrib_norm = gl.getAttribLocation(program, "norm");
        gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
        gl.enableVertexAttribArray(attrib_pos);
        gl.enableVertexAttribArray(attrib_norm);
        gl.vertexAttribPointer(attrib_pos, 4, gl.FLOAT, false, 8 * 4, 0);
        gl.vertexAttribPointer(attrib_norm, 4, gl.FLOAT, false, 8 * 4, 4 * 4);

        let attrib_instance_transform = gl.getAttribLocation(program, "instance_transform");
        gl.bindBuffer(gl.ARRAY_BUFFER, instance_buffer);
        for (let i = 0; i < 4; i++) {
            gl.enableVertexAttribArray(attrib_instance_transform + i);
            gl.vertexAttribPointer(attrib_instance_transform + i, 4, gl.FLOAT, false, 16 * 4, 16 * i);
            gl.vertexAttribDivisor(attrib_instance_transform + i, 1);
        }



        // MAIN LOOP //
        let running = null;

        let key_pressed = {};
        window.addEventListener("keydown", function (e) { key_pressed[e.code] = true });
        window.addEventListener("keyup", function (e) { key_pressed[e.code] = false });

        let time;

        // Move the mouse to look around.
        canvas.addEventListener("mousemove", function (e) {
            transform = matrix_mul([
                Math.cos(e.movementX / canvas_size), 0, -Math.sin(e.movementX / canvas_size), 0,
                0, 1, 0, 0,
                Math.sin(e.movementX / canvas_size), 0, Math.cos(e.movementX / canvas_size), 0,
                0, 0, 0, 1,
            ], transform);

            transform = matrix_mul([
                1, 0, 0, 0,
                0, Math.cos(e.movementY / canvas_size), Math.sin(e.movementY / canvas_size), 0,
                0, -Math.sin(e.movementY / canvas_size), Math.cos(e.movementY / canvas_size), 0,
                0, 0, 0, 1,
            ], transform);
        });

        function animation_frame_callback(t) {
            running = null;

            if (!time) { time = t; }
            // time since last frame, in seconds
            let dt = (t - time) / 1000;
            time = t;

            // Use WASD, Space, and Shift to move around.

            // Move forward
            if (key_pressed["KeyW"]) {
                transform = matrix_mul([
                    1, 0, 0, 0,
                    0, 1, 0, 0,
                    0, 0, Math.cos(dt), -Math.sin(dt),
                    0, 0, Math.sin(dt), Math.cos(dt)
                ], transform);
            }
            // Move backward
            if (key_pressed["KeyS"]) {
                transform = matrix_mul([
                    1, 0, 0, 0,
                    0, 1, 0, 0,
                    0, 0, Math.cos(dt), Math.sin(dt),
                    0, 0, -Math.sin(dt), Math.cos(dt)
                ], transform);
            }
            // Move down
            if (key_pressed["ShiftLeft"]) {
                transform = matrix_mul([
                    1, 0, 0, 0,
                    0, Math.cos(dt), 0, -Math.sin(dt),
                    0, 0, 1, 0,
                    0, Math.sin(dt), 0, Math.cos(dt)
                ], transform);
            }
            // Move up
            if (key_pressed["Space"]) {
                transform = matrix_mul([
                    1, 0, 0, 0,
                    0, Math.cos(dt), 0, Math.sin(dt),
                    0, 0, 1, 0,
                    0, -Math.sin(dt), 0, Math.cos(dt)
                ], transform);
            }
            // Move right
            if (key_pressed["KeyD"]) {
                transform = matrix_mul([
                    Math.cos(dt), 0, 0, Math.sin(dt),
                    0, 1, 0, 0,
                    0, 0, 1, 0,
                    -Math.sin(dt), 0, 0, Math.cos(dt)
                ], transform);
            }
            // Move left
            if (key_pressed["KeyA"]) {
                transform = matrix_mul([
                    Math.cos(dt), 0, 0, -Math.sin(dt),
                    0, 1, 0, 0,
                    0, 0, 1, 0,
                    Math.sin(dt), 0, 0, Math.cos(dt)
                ], transform);
            }

            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
            gl.uniformMatrix4fv(gl.getUniformLocation(program, "transform"), false, transform);
            gl.uniformMatrix4fv(gl.getUniformLocation(program, "perspective"), false, perspective);
            gl.uniform4fv(gl.getUniformLocation(program, "lightPositions"), Float32Array.from([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]));
            gl.drawArraysInstanced(gl.TRIANGLES, 0, mesh_data.length / 8, instance_data.length / 16);

            running = window.requestAnimationFrame(animation_frame_callback);
        };

        document.addEventListener('pointerlockchange', function (e) {
            if (running) {
                window.cancelAnimationFrame(running)
            }
            if (document.pointerLockElement === canvas) {
                key_pressed = {};
                running = window.requestAnimationFrame(animation_frame_callback);
            }
        }, false);

    </script>
</body>

</html>