Summary Files Commits Branches @commit @diff @stats
path: root / src / blog
diff options
context:
space:
mode:
Commit29090187820c880e3f712a18d66482c4a7fd9ff5 (patch)
Author
 Syndamia <kamen@syndamia.com>
2023-12-26 17:48:36 +0200
Committer
 Syndamia <kamen@syndamia.com>
2023-12-26 17:48:36 +0200
Files77f4df2f7754edfdab1a681ac8b9f7e720fbd8fb /src/blog
syndamia.com-29090187820c880e3f712a18d66482c4a7fd9ff5.tar
syndamia.com-29090187820c880e3f712a18d66482c4a7fd9ff5.tar.gz
syndamia.com-29090187820c880e3f712a18d66482c4a7fd9ff5.zip
Parentc2f3a63ed8da8490071972c8fd9d34be35d34b48 (diff)
(blog/bhs) Added initial contents of webgl and shaders sections
1 files changed, 156 insertions, 3 deletions
-rw-r--r--src/blog/difficulties-of-making-a-black-hole-shader/index.md159
diff --git a/src/blog/difficulties-of-making-a-black-hole-shader/index.md b/src/blog/difficulties-of-making-a-black-hole-shader/index.md
index 17304c6..70c711e 100644
--- a/src/blog/difficulties-of-making-a-black-hole-shader/index.md
+++ b/src/blog/difficulties-of-making-a-black-hole-shader/index.md
@@ -18,7 +18,7 @@
Back in August I had to submit a project for an elective WebGL programming course in university.
I chose to create an interractive and animated black hole demo.
-The primary idea was to show how the gravitational pull of black holes "bend" light and how the result looks from different points of view.
+The primary idea was to show how the gravitational pull of black holes "bends" light and how the result looks from different points of view.
During development I found a lot of hurdles, primarily with the actual mathematics behind it.
Information online is difficult to find and understand if you don't have a physics background, so I'll try my best to lay out everything I know.
@@ -33,7 +33,7 @@ Image and video files store visual information in a "fixed" way: you get a huge
The objects in those mediums cannot be modified easily, and additions or subtractions are even harder to do, usually requiring an expert using professional tools for hours, days or weeks on end.
Computer graphics aims to offload a lot of the creator's workload by "generating" the images on the fly using a set of descriptions for our objects.
-The idea is to define objects with some the primary properties of simpler geometric figures, like the points on a triangle, then give those to the computer, which will in turn have to calculate what the precise color of every pixel in the resultant image should be.
+The idea is to define objects with some primary properties of simpler geometric figures, like the points on a triangle, then give those to the computer, which will in turn have to calculate what the precise color of every pixel in the resultant image should be.
For example, we can represent a tin can (without a label) as a simple cyllinder.
Changing it's height or width is trivial: just change the corresponding property you pass to the computer, and it will handle the rest.
@@ -68,6 +68,9 @@ You would get a DOM object in which the graphics will be rendered, and a "contex
## Theoretical and algorithmic background
+&&important I haven't formally studied physics, so you must read with scepticism in mind.
+ There are most definetly errors, steming from my lackluster understanding of the subject.
+
Using [Einstein's field equations](TODO), or to simplify our life, [Schwarzschild's metric](TODO), we can precisely calculate the curvatures in space(time) at every point around a Schwarzschild black hole.
Unfortunetly, the computation would still be too laborious, so we'll need to make an approximation using [celestial mechanics](TODO).
@@ -124,6 +127,10 @@ We can now also define what it means to "go straight forwards": in flat surfaces
Going back to physics, what the General theory of relativity tells us is that objects with mass bend the physical space, or 3D surface, of the universe, and we call that gravity.
As a result, the geodesics too are curved, so even though you may be always moving forwards at the same speed, to an outside observer it will look like you turn and change speed.
+The explanation for modified speed is simpler: since we are travelling at the same speed, from the point of view of an outside observer, it should take the same amount of time to go between points P and Q, regardless of gravity.
+However, a normal straight line segment through the points is always shorter than the gravitationally-induced circular arc, meaning it will look like you are travelling a greater distance.
+The only way to balance everything out, is to travel faster.
+
The amount by which and the size of the area which is affected depend on that object's mass and size.
Generally, if two objects have the same mass, the bigger one will affect a larger portion of space, but will bend it less, compared to the smaller one, which will affect a smaller portion, which will be bend a lot.
@@ -306,7 +313,151 @@ as a reminder, here is the definiton of <code>F<sup>&rarr;</sup></code>:
## WebGL preparation
-## Shader
+The course was focused on handmade solutions, so some of the logic uses a [custom handmade library](TODO).
+
+Since the primary component was the shader itself, I made sure to simplify my logic to the absolute minimum.
+A **lot** of decisions were made to optimize development time, without considering how good they were.
+
+In a scene there really are only 3 distinct objects: the stars, the black holes and the skybox.
+The latter is just a simple cube, inside which everything resides, if a ray doesn't hit anything of interest, it will hit the cube.
+Both stars and black holes are simple spheres, the former with an orange-y color (#f4803b) and any radious.
+Black holes aren't rendered and their underlying sphere has the same radius as the ISCO, so their purpose is for intersection checks (more on those when we get to the [shaders](#shaders)).
+
+Most JavaScript code isn't worth talking about, wherever possible I use the [course library](TODO) and where I can't it's mostly simple WebGL configurations, elementary object "classes" and trivial HTML handling.
+Though, there are still two somewhat interesting systems:
+
+### Scenes
+
+I wanted to have multiple scenes, with different arrangements of objects and with animations (stars moving around).
+For that I devised a dead-simple level loading and animation system.
+
+An object of type "Level" stores a name, description, location of camera (the viewer) and arrays for stars, black holes and animations.
+Though, for creating new levels, you only pass the first three:
+
+```javascript
+var level1 = new Level(
+ "Star passing behind black hole", // Level title
+ "The black hole warps the light", // Level description
+ [1000,0,650]); // Initial coordinates of camera
+```
+
+Adding stars or black holes requires going through two member functions: `addStar` and `addBlackHole`.
+Both take the center of the object, as well as it's radius, alongside an animation function.
+The latter has two parameters: current time in TODO(seconds,milliseconds,?) from the beginning and the current object that will be created.
+This function is called on each frame with the appropriate arguments, so inside you'll have to "manually" modify the object's properties.
+
+For example, a simple star going forwards could be added like this (`vecSum` is just [componentwise vector summation](TODO)):
+
+```javascript
+level1.addStar([1070,0,1000], // Coordinates of center
+ 10, // Radius
+ (t, star) => vecSum(star.center, [-t,0,0])); // Move from left to right
+```
+
+Or one which rotates around a point:
+
+```javascript
+var speed = Math.PI / 20, rad = 40;
+level1.addStar([1000+rad,0,900],
+ 3,
+ (t, star) => vecSum([1000,0,900], [Math.cos(t*speed) * rad,
+ Math.sin(t*speed) * 5,
+ Math.sin(t*speed) * rad]));
+```
+
+For black holes it's the same.
+If you want the object to not move, you could just pass `undefined` as the function:
+
+```javascript
+level1.addBlackHole([1000,0,900], 30, undefined);
+```
+
+### Movement
+
+It wouldn't be fun without the ability to move around in the scene, so I added a simple system for that too.
+WASD moves the camera position forward, left, backward and right, shift moves down and space moves up, all relative to the current direction the camera is looking towards.
+Those are all done with relatively simple analytic geometry: we store three vectors, one pointing up, one pointing forward and one pointing left (which can be calculated from the other two with a cross product).
+To move forward, add to the position the forward vector times some constant, to move backwards subtract and so on.
+
+Looking around with the mouse was kinda tricky, since I decided to use [spherical coordinates](TODO).
+You store two angles, the "up" and "left" angle, moving the mouse forwards adds to the "up" angle, moving it downwards subtracts and so on.
+The tricky part was what to do after
+
+TODO
+
+## Shaders
+
+As a gentle reminder, in OpenGL we have a vertex shader and a fragment shader:
+
+- The vertex shader is ran on every point of every object in our scene
+- The fragment shader is ran on every pixel of your screen, with the added bonus that depending on the pixel, certain values are passed down from the vertex shader.
+
+ This includes interpolated points: a line is made up of two points, the vertex shader is ran on both of them, then the line is interpolated and the fragment shader is ran on each pixel, corresponding to a point on the line.
+ The vertex shader values are also passed down interpolated, so if one end of the line is the color red, while the other end is green, then the fragment shader corresponding to the pixel, which shows the middle of the line, will have the color yellow.
+
+### Vertex shader
+
+As already mentioned, black holes aren't rendered, so the only objects with special rendering are the stars.
+To simplify my life, I chose to do a simple [diffusion reflection](https://learnopengl.com/Lighting/Basic-Lighting), with the light source emminating from the camera, something which was already made in lectures:
+
+![](TODO)
+
+The result isn't amazing, but you can clearly make out the sphere as a 3D object (not a flat circle).
+
+### Fragment shader
+
+This is the meat of our logic, this is where we'll implement our algorithm:
+
+![](./img/algorithm.png)
+
+In real life, every star emits light, which then travells some distance, until it reaches our eyes.
+However, computing it this way is incredibly inefficient, for every star we'll have to create hundreds or thousands of light rays (corpiscules), most of which probably wont reach us.
+We want performance, so this is a no-go.
+
+The alternative is to do the opposite, assume every pixel was hit by a "ray" and trace back it's steps, if it came out of a star then it has some specific color (it was an actual ray), but if it didn't then show it as black (no ray hit us).
+Taking it further, how do we "trace back" those steps?
+Well, we can find the precise coordinate of every pixel inside the 3D scene, and move each point in the direction we're looking (though this image is about raytracing, just look at the two top red vectors):
+
+![](https://upload.wikimedia.org/wikipedia/commons/thumb/5/5c/Ray_trace_diagram.png/800px-Ray_trace_diagram.png?20080214101526)
+*Image by Timrb on [Wikipedia](https://commons.wikimedia.org/w/index.php?curid=3553478)*
+
+In the context of computer graphics, this is called [ray marching](TODO).
+
+Now, how do we detect if the ray hit something?
+The dumb way is to move the ray forward a bit and check with every object whether the ray intersects it.
+However, since all of our objects are spheres, there is a very easy optimization and simplification - imagine the ray as an infinitely long line and just get the shortest (perpendicular) distance between it and the center of the sphere:
+
+TODO custom image #1
+
+For this we can implement a simple distance function:
+
+![](https://wikimedia.org/api/rest_v1/media/math/render/svg/42c53741fd3d8765e56ea652369a512a2cd09eac)
+
+```glsl
+float linePointDistance(vec3 a, vec3 n, vec3 p) {
+ return length((p - a) - dot(p - a, n) * n);
+}
+```
+
+Where `a` is the coordinate of our ray (pixel), `n` is the direction it will be travelling in and `p` is the sphere center we're comparing with.
+
+However, there is a huge problem: our line extends in both directions, so a check like `linePointDistance(ray, step, center) <= radius` would also detect if the ray interects the point behind us (when ray is going in the opposite direction)!
+A very simple solution is to just add a sign to our distance: if positive then the distance is in reference to a point in front, while negative means it's behind.
+The simplest way to get this information is by taking the [dot product](TODO) between our "forwards" direction and the direction from our coordinates to the [projection](TODO) of our point on the line:
+
+TODO custom image #2
+
+```glsl
+float linePointDistance(vec3 a, vec3 n, vec3 p) {
+ vec3 projection = dot(p - a, n) * n;
+ float dist = length((p - a) - projection);
+
+ if (dot(projection, n) < 0.0) return -dist;
+ return dist;
+}
+```
+
+Since all of our objects are spheres, we can just iterate through all of them and do this check.
## Appendix
@@ -369,9 +520,11 @@ The images for my custom LaTeX equations were created on [latexeditor.lagrida.co
\end{align*}
```
+[^matter-together]: Technically, it's more accurate to say "matter apart", since after the collapse matter is as close together as physically possible, and the nuclear forces keep everything in order but also separated. However, considering the intuitive approach I'm striving for, "matter together" is simpler to understand.
[^parallels-of-latitude]: You might think about the latituginal lines on a globe, they are indeed parallel (equidistant), but if you take any two points on the same such line, the distance between them won't be the smallest.
[^geodesics-are-local]: This actually isn't completely true, a geodesic is the "locally shortest distance", I've skipped the details since they aren't really that important for our purpopses. More info can be found on [wikipedia](https://en.wikipedia.org/wiki/Geodesic#Introduction).
[^schwm-assumptions]: He also assumed the body didn't have an electric charge and the [cosmological constant](TODO) is zero. These are way outside of my understanding and our purposes, so I skiped them for simplicity.
[^isco]: It's not really true that gravity stopps affecting spacetime after the ISCO, even though things can orbit Earth that doesn't mean that Earth doesn't bend spacetime. However, for the purposes of a simple shader, this assumption produces perfectly useable results.
+[^hitting-trains]: Of course, I'm asuming the trains are strong enough they won't break upon the collision.
[^angular-momentum]: Angular momentum is actually [defined using the cross product](https://en.wikipedia.org/wiki/Specific_angular_momentum#Definition)
[^light-has-no-mass]: Actually, light has no mass, so this version of the formula wouldn't work. You might think that makes no sense, since we cannot divide by zero, so the specific angular momentum shouldn't exist, but then with the cross product we find a value for it. You would be correct: Antonelli does some funky approximations, combining Newtonian physics with General theory of relativity, to make the formula that simple. Someone in physics will probably vomit looking at it, but for our purposes it's more than perfect.