BRK3VS5SZ2S24HNMTRTHGP4LDV2A63RXU4C6LITQATBI34KRECLQC
# Full Example: Spherical Renderer
```html
{{#include ../examples/spherical.html}}
```
# Full Example: Euclidean Renderer
```html
{{#include ../examples/euclidean.html}}
```
# Rendering: The Easy Case
In this chapter, I will explain the easiest case of non-Euclidean rendering: spaces that are homogeneous and isotropic.
This includes the most familiar types of non-Euclidean geometry: spherical and hyperbolic.
Why do I say this is easy? It turns out that OpenGL is just as happy to process these spaces as it is Euclidean geometry.
You don't need to switch to raytracing, or use complicated hacks, like you would for weirder spaces.
I plan to show you everything you need to know to render a simple scene using WebGL.
As a prerequisite, I will assume you understand how to do this in Euclidean geometry.
## Euclidean Example
Let's begin by looking at an example in Euclidean geometry. I'll just go over the interesting parts, but the full example is available [here](example_euclidean.html).
Here's my vertex shader.
```js
{{#include ../examples/euclidean.html:97:118}}
```
It's fairly standard. But notice that I chose the `vec4` representation:
- A point is stored as a `vec4` whose last coordinate is 1.
- A vector is stored as a `vec4` whose last coordinate is 0.
- A transformation is stored as a `mat4`s whose last row is `0 0 0 1`.
This will ease the comparison to the non-Euclidean case later.
Some other minor notes:
- The shader outputs are in a player-centric coordinate system. This is just because I didn't want to deal with matrix inverses.
- `instance_transform` exists because I am using instanced rendering.
Now here's the fragment shader.
```js
{{#include ../examples/euclidean.html:120:163}}
```
Again, fairly standard. Ambient lighting, plus three sources of diffuse lighting.
Finally, the motion-handling code is fairly simple.
To move or rotate, just multiply by the relevant matrix.
<details>
<summary>Rotation</summary>
Note: The matrices are stored in column-major order, so they look like they're transposed.
```js
{{#include ../examples/euclidean.html:218:233}}
```
</details>
<details>
<summary>Motion</summary>
Note: The matrices are stored in column-major order, so they look like they're transposed.
```js
{{#include ../examples/euclidean.html:245:298}}
```
</details>
## Making it Non-Euclidean
All of this should be familiar, seen-it-before rendering code.
Now let's modify it to be non-Euclidean!
Specifically, let's render spherical geometry.
### Points and Vectors
The first thing to understand is how to represent points and vectors.
In Euclidean space, a point `p` was a `vec4` satisfying the equation `p.w = 1`.
In spherical space, a point `p` will instead be a `vec4` satisfying the equation `dot(p,p) = 1`.
Vectors are trickier.
In Euclidean space, we're used to having a consistent concept of direction, independent of your location.
But that doesn't work in spherical geometry.
Imagine you're at the north pole, looking down the 90°W meridian. You travel forward ten thousand kilometers.
You arrive at the Galapagos Islands, facing south.
Next, without turning, you travel rightward ten thousand kilometers.
You arrive in Kiribati (a country north-east of Australia), still facing south.
Finally, you back up ten thousand kilometers.
You are back at the north pole. But now you are looking down the 180°W meridian!
You never turned during the trip, yet you find yourself rotated by 90 degrees.
This phenomenon is called *holonomy*, and it occurs in any non-Euclidean space.
It means the concept of direction *depends on your location*.
If we draw two arrows right next to each other, we can obviously tell whether they're pointing in the same direction.
But if the arrows are far apart, it's less clear. We could try carrying one arrow over to the other, but then the answer would depend on which path we took.
A vector is just a length and a direction. So it also depends on your location.
Each point in our space comes with its own collection of tangent vectors.
We call this collection the *tangent space* of that point.
Let's return to our question:
*<p style="text-align: center;">How should we represent vectors?</p>*
We now know that this question should be rephrased:
*<p style="text-align: center;">At a given point `p` of our space, how should we represent its collection of tangent vectors?</p>*
I can now give an answer. A vector `v`, in the tangent space of point `p`, will be a `vec4` satisfying the equation `dot(p,v) = 0`.
### Computations
In any particular tangent space, vectors can be added, scaled, and dotted, exactly as normal.
But you shouldn't try to add or dot vectors from *different* tangent spaces.
Adding a point to a vector, or subtracting two points to get a vector, are a bit sketchier.
They're approximately correct for nearby points and small vectors, but they get worse as the points and vectors get larger.
For example, if `p` is a point, and `v` is a vector at that point, then `p + v` isn't quite a point:
```text
dot(p + v, p + v)
= dot(p, p + v) + dot(v, p + v)
= dot(p, p) + dot(p, v) + dot(v, p) + dot(v, v)
= 1 + 0 + 0 + dot(v, v)
= 1 + dot(v, v)
```
But if `v` is small, it might be close enough.
The distance between points `p` and `q` is given as `acos(dot(p, q))`.
A vector in the tangent space of `p`, pointing toward `q`, is given as `q - p * (dot(p,q)/dot(p,p))`.
### Vertex Shader
All right, let's start modifying the renderer! First up: the vertex shader.
And we're done!
That's right, *no changes need to be made*.
The data flowing through will be different. Instead of `pos.w = 1` and `norm.w = 0`, we'll have `dot(pos, pos) = 1` and `dot(pos, norm) = 0`.
Instead of a transformation matrix whose last row is `0 0 0 1`, we'll have a transformation matrix satisfying `M * Mᵀ = Mᵀ * M = 1`.
But the vertex shader doesn't care. It'll happily apply transformation matrices to points and vectors, no matter which geometry you're using.
```js
{{#include ../examples/spherical.html:110:131}}
```
### Fragment Shader
The fragment shader is more interesting, because it does the lighting calculations.
Consider the diffuse lighting from a particular light source.
We have to calculate the light intensity at the surface, and the angle at which the light hits the surface.
Both of those calculations work differently in spherical space.
First, consider the light intensity.
In Euclidean space, it's well known that the intensity of light is inversely proportional to the square of the distance.
This is because the light spreads out in all directions, and the surface area of a sphere is 4πr².
In spherical space, however, the surface area of a sphere is 4πsin²(r).
So if the light source is at point `p`, the light intensity at `q` should be inversely proportional to:
```text
4 * π * sin²(distance(p,q))
4 * π * sin²(acos(dot(p,q)))
4 * π * (1 - cos²(acos(dot(p,q))))
4 * π * (1 - (dot(p,q))²)
```
There is an interesting special case when `p ≈ -q`. Even though the surface is far from the light source, the light intensity is large.
This is because the curved space acts like a lens, focusing all of the light's energy back onto a point.
Now consider the angle calculation.
As in the Euclidean case, the computation is based on the dot product of two vectors: the normal, and the direction to the light source.
*Unlike* the Euclidean case, though, we need to be careful that both vectors are in the tangent space of the surface.
With these considerations in mind, here's my modified fragment shader.
```js
{{#include ../examples/spherical.html:133:177}}
```
### Transformation Matrices
A valid transformation matrix is one that turns valid points into valid points.
In Euclidean space, that's any matrix with last row `0 0 0 1`:
```text
⎡a b c d⎤ ⎡x⎤ ⎡a*x + b*y + c*z + d*1⎤ ⎡ax + by + cz + d⎤
⎢e f g h⎥ ⎢y⎥ === ⎢e*x + f*y + g*z + h*1⎥ === ⎢ex + fy + gz + h⎥
⎢i j k l⎥ ⎢z⎥ === ⎢i*x + j*y + k*z + l*1⎥ === ⎢ix + jy + kz + l⎥
⎣0 0 0 1⎦ ⎣1⎦ ⎣0*x + 0*y + 0*z + 1*1⎦ ⎣ 1 ⎦
```
Matrices of this form look include rotations, reflections, translations, scalings, and shear transformations.
In spherical space, on the other hand, that's any matrix with `M * Mᵀ = Mᵀ * M = 1`:
```text
dot(M * v, M * v)
= dot(v, M * Mᵀ * v)
= dot(v, v)
= 1
```
Spherical rotation matrices look the same as Euclidean ones.
```text
⎡ cos(θ) 0 sin(θ) 0 ⎤
⎢ 0 1 0 0 ⎥
⎢-sin(θ) 0 cos(θ) 0 ⎥
⎣ 0 0 0 1 ⎦
```
But spherical translation matrices don't. In fact, they look kind of like rotation matrices:
```text
⎡ cos(θ) 0 0 sin(θ) ⎤
⎢ 0 1 0 0 ⎥
⎢ 0 0 1 0 ⎥
⎣-sin(θ) 0 0 cos(θ) ⎦
```
Just as in the Euclidean case, transformation matrices work on both points and vectors.
If `v` is in the tangent space of `p`, then `M*v` will be in the tangent space of `M*p`.
### Perspective Matrices
It turns out that perspective matrix formula works as normal, except you have to input `tan(znear)` and `tan(zfar)` instead of `znear` and `zfar`.
The maximum view distance is π, though this may be cut in half if you're using a library that "helpfully" requires the `zfar` input to be positive.
### Mesh Data
When changing the geometry, you will, of course, have to redesign the mesh and move the lights.
When doing so, keep in mind the note below.
## A Note on Distances
In Euclidean games, we're used to choosing whatever units we like.
Maybe we choose that "one unit" is a meter, or a centimeter.
It doesn't really matter, so you can choose whatever is convenient.
In spherical space, however, it really does matter. The universe is 2π units around, so don't make the player character 2 units tall!
# Summary
- [Rendering: The Easy Case](./easy.md)
- [Full Example: Euclidean Renderer](./example_euclidean.md)
- [Full Example: Spherical Renderer](./example_spherical.md)
<!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>
<!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.45, +0.55, +0.45, 1, /* norm */ 0, 0, -1, 0,
/* pos */+0.45, +0.45, +0.45, 1, /* norm */ 0, 0, -1, 0,
/* pos */-0.45, +0.45, +0.45, 1, /* norm */ 0, 0, -1, 0,
]);
// 3000 transformation matrices.
let instance_data = function () {
let matrices = Array();
[-1, 1].forEach(j => [-1, 1].forEach(k => {
for (let x = -2; x <= 2; x++) {
for (let y = -2; y <= 2; y++) {
for (let z = -2; z <= 2; z++) {
[j * k, 0, 0, 0, 0, j, 0, 0, 0, 0, k, 0, x, y, z, 1].forEach(val => { matrices.push(val); });
[j * k, 0, 0, 0, 0, 0, j, 0, 0, k, 0, 0, x, y, z, 1].forEach(val => { matrices.push(val); });
[0, j * k, 0, 0, 0, 0, j, 0, k, 0, 0, 0, x, y, z, 1].forEach(val => { matrices.push(val); });
[0, 0, j * k, 0, 0, j, 0, 0, k, 0, 0, 0, x, y, z, 1].forEach(val => { matrices.push(val); });
[0, 0, j * k, 0, j, 0, 0, 0, 0, k, 0, 0, x, y, z, 1].forEach(val => { matrices.push(val); });
[0, j * k, 0, 0, j, 0, 0, 0, 0, 0, k, 0, x, y, z, 1].forEach(val => { matrices.push(val); });
}
}
}
}));
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 = 0.1
// zfar = -0.1
// Note: Visually, a negative zfar value has the same result as an infinite one.
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 pointing from v to u.
'vec4 directionTo(vec4 u, vec4 v) {\n' +
' return u - v;\n' +
'}\n' +
// If there is a light at u, how much light reaches v?
'float lighting(vec4 u, vec4 v) {\n' +
' return 1. / dot(u-v, u-v);\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.6 * 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.6 * 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.6 * 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, 1, 0,
0, 0, dt, 1
], transform);
}
// Move backward
if (key_pressed["KeyS"]) {
transform = matrix_mul([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, -dt, 1
], transform);
}
// Move down
if (key_pressed["ShiftLeft"]) {
transform = matrix_mul([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, dt, 0, 1
], transform);
}
// Move up
if (key_pressed["Space"]) {
transform = matrix_mul([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, -dt, 0, 1
], transform);
}
// Move right
if (key_pressed["KeyD"]) {
transform = matrix_mul([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
-dt, 0, 0, 1
], transform);
}
// Move left
if (key_pressed["KeyA"]) {
transform = matrix_mul([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
dt, 0, 0, 1
], 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, 1, 0, 1, 0, 1, 0, 0, 1, 1]));
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>
[book]
authors = ["finegeometer"]
language = "en"
multilingual = false
src = "src"
title = "On Making Non-Euclidean Games"
book