Sunday, July 4, 2021

Catching a fall (procedurally.)

I have been doing a lot of procedural animation, lately. Making a humanoid walk, and swing a torch at spiders.

I also needed a proper way to catch a fall, with the legs. Because I already had the Inverse Kinematics sorted, I just needed to shift the pelvis down, and up, after a fall.

To do this convincingly, I need to drop down hard at the start, then ease at the nadir, and ease again on the way back to the zenith. For this, piecewise functions come to the rescue. I ended up using this:

Monday, June 21, 2021

Field Of View

Computer monitors come in different aspect ratios. From the old school 4:3 to the ridiculous 32:9.

In my 3D games, I have to choose a Field-Of-View for the camera. Wide angle, or narrow angle views? To do this, the convention is to set a fixed vertical field of view, and then adopt the horizontal field of view that falls out of it, based on the image aspect ratio. I use this for all my 3D games: set the vertical F.o.V.

Fixing the Vertical Field of View, means that users on 4:3 monitors see less of the world, and users on ultra wide screen see more of the world. This could pose an unfair advantage.

An additional problem is that ultra wide aspect ratios lead to ultra wide angle views, which lead to a lot of distorted shapes in the corners of the screen.

But fixing the Horizontal Field of View instead, would mean that users on wide-screen would see less of the world (at top and bottom parts of screen.) This made me wonder... what if I were to fix the angle for the diagonal field instead? Fixed for all users. And then let the Horizontal/Vertical angles fall out of this.

Definitely worth an experiment! So let's do some trigonometry!

Problem: given a fixed angle for the diagonal field of view... what are the corresponding Vertical and Horizontal fields of view?

Given the fixed diagonal field of view angle α, and a given image height h, and image diagonal d, we need merely two equations to derive the vertical field of view angle β.

And with that angle β we can set up our projection matrix the usual way.

When we do all this, we go from extremist views (with fixed vertical FoV) like these:

...to more moderate views (with fixed diagonal FoV) like the ones below. Note that the view angles change less than in the case above, between aspect ratios. The narrow aspect ratio does not show that much less on left/right, and for what it loses on the sides, it wins a little on top/bottom. I think this approach is a keeper!

For completeness, the code, which is trivial:
float vertical_fov_from_diagonal_fov( float a, float w, float h )
{
        const float d = sqrtf(w*w + h*h);
        const float m = d / ( 2 * tanf( a / 2 ) );

        const float b = 2 * atanf( h / (2*m) );
        return b;
}
  

Monday, June 14, 2021

Fire in OpenGL

(Just 120 particles, looks better in 60Hz and without GIF artifacts.)

This is a quick 'n dirty approach for getting a fire visualization using OpenGL.

Fire blends somewhat with its background, so we will be using blending. But unlike regular transparency, fire works a little different: it only adds light to your framebuffer, never removes it. For this, the blendmode GL_ONE, GL_ONE is perfect!

Ok, now we know how to draw it, which leads to the question, on what to draw. I find that the majority of approaches you find on the web go for textured billboard. But I think that it is better to go with geometry instead, that is un-textured. By adding some shape to the geometry that you draw, you can save on the number of particles you need to draw. I opted for a spikey kind of vortex, as shown below.

From the picture above you can see there is already an inherent counter-clockwise rotation in the model, which we should replicate in the animation. So as we draw each particle, we should keep rotating the billboard we draw for it. Of course, the billboard needs to be oriented to the camera as well! Which means we have to do a little math in the vertex shader.

#version 150

in mediump vec4 position;	// per vert: vertex position.

in mediump vec4 bdpos;		// per inst: bill board pos.
in mediump vec4 rgb;		// per inst: colour.
in mediump float scale;		// per inst: scale.
in mediump float angle;		// per inst: angle.

out lowp vec3 colour;		// to fragment shader: colour.

uniform highp mat4 modelcamviewprojmat;
uniform highp mat4 camtrf;

void main()
{
	vec4 camx   = camtrf[0];
	vec4 campos = camtrf[3];
	highp vec4 scaledpos;
	scaledpos.xyz = position.xyz * scale;
	scaledpos.w = 1.0;
	vec3 z = bdpos.xyz - campos.xyz;
	z = normalize(z);
	vec3 camy = camtrf[1].xyz;
	vec3 xx = cross( camy, z );
	vec3 yy = cross( xx, z );
	vec3 x = xx *  cos(angle) + yy * sin(angle);
	vec3 y = xx * -sin(angle) + yy * cos(angle);
	mat4 trf = mat4(1.0);
	trf[0].xyz = x;
	trf[1].xyz = y;
	trf[2].xyz = z;
	trf[3] = bdpos;
	highp vec4 tpos = trf * scaledpos;
	colour = rgb.rgb;
	gl_Position  = modelcamviewprojmat * tpos;
}
  

Let us break down that vertex shader. Like any vertex shader for 3D graphics, it transforms a vertex (position) using a modelviewprojection matrix, no surprised there.

All the other inputs for this shader are per-instance attributes, not per-vertex attributes. So you need to call glVertexAttribDivisor() for them, with value 1.

Every particle billboard has a position (bdpos,) a colour (rgb,) a size (scale,) and a rotation (angle.)

The scale we just apply to the model's vertex position before we transform the vertex to clip-space. And the colour is passed onto the fragment shader, as-is.

Note that we don't just pass the modelviewprojection matrix, but also a second matrix: the camera transformation. This is just the view matrix, before it got inverted from camera to view. We need this, so that we can reconstruct a proper orientation for our billboard. We create a model transform for the billboard with Z pointing to our camera. This is basically (xx,yy,z, bdpos). But we skew the xx,yy axes with the particle's rotation angle, so that we end up with the (x,y,z, bdpos) transform. To skew them, the new x,y are both just a linear combination of the old xx,yy with the cos/sin factors.

Once we applied, this per-particle transformation to the (scaled) vertex position, we get our transformed position (tpos.) That, can finally be transformed with the modelviewproj matrix to give us our final result.

That covers the GPU-side of things. But we still need to fill in the proper rgb/scale/angle values on the CPU-side.

The scale is easy: because gas disperses as it burns, we just have to make the particle grow in size, as it ages. The angle is easy too, I just apply a rotational velocity that scales with the linear velocity of the particle, always in the same direction (counter clockwise.)

That leaves us with the colours. We use GL_ONE, GL_ONE, and it is best to use low colour values, so that sharp boundaries between particle and no particle are not too obvious. Also, we should slowly fade-out our particles. So as the particle ages, I make it go fainter! And for the chromatic transitions, just make the particle go from whitish, to yellowish to reddish, and you should be good.

I am using this approach in my project for Real Time Global Illumination, where I use the particles as light sources.

Monday, May 24, 2021

Things that farming Chia will make you aware of.

When you farm Chia, the proof-of-space blockchained coin, you learn a few new things.

  • Which PCIe generation your motherboard offers
  • NVMe performance
  • NVMe endurance
  • The fstrim command
  • XFS options like crc=0
  • What the term JBOD means
  • The performance of NFS over your LAN
  • The thermal characteristics of your drives
  • The difference between peak write bandwidth and sustained write bandwidth
  • The Crucial P1 and PNY XLR8 have incredibly slow write bandwidth. Avoid!

So far, this has been a fully educational excercise, with zero financial pay-off: My 6 drives can't compete with the netspace. But stream-lining your computer for plotting was a fun exercise in optimization.

If nothing else, I at least got a rather popular Open Source Tool out of it, in my portfolio: ChiaHarvestGraph and its sister tools ChiaPlotGraph and ChiaHeightGraph.

Tuesday, May 4, 2021

Directional Light

I have implemented directional light for Practica Boxel. It simulates a harsh distant sun.

Because there is only one bounce, there are fully black shadows with 0% light in the scene. I'm not sure I like it.

It's main advantage is that it is easier to render in chunks, as there is always just one lightsource, which is always the same. No need to cull lightsources, which simplifies things a lot. As local lightsources pop into view, the delta in light is too jarring, so this circumvents that.

I've decided to go back to my original experiments where I have a night-scene with just 1 torch light.

And with one hand-held light source, I find that a first-person camera is a no-go: because if light and camera are close to eachother, the lighting becomes un-interesting, no shadows, not much indirect light, so the whole GLobal Illumination is wasted on it.

Which leaves me with...

A night-time world, one hand-held lightsource (torch) and a third-person camera.

Think: A 3D Ant Attack clone, set at night, and with Global Illumination.

So, when doing a local point-light, I should modify how I shoot my photons again! I started with a cosine weighted hemi-sphere direction. Then I added directional light. And for a torch, I would need a uniform omni-directional light.

So, blue-noise sampling of a sphere it is, then. Alan Wolfe came to the rescue with his suggestion of using Mitchell's Best Candidate algorithm to create progressive blue noise.

As his approach was O(N³) I had to weaken it a bit, but reducing the number of candidates. I implemented it in C, using 16-threads and also 16-wide SIMD for conceptually handling 256 candidates simultaneously. By leaving it running over-night, with my CPU at full blast, I was able to create a data-set of 2M samples that look reasonably like blue-noise.

2 million sample points, viewed from the inside:

So, night-time-Ant-Attack, here I come...

Monday, April 12, 2021

Partwise approach to Global Illumination?

With the implementation of Practica Boxel I was able to verify that for simple (aab primitives) and small (200 blocks) scenes, you can compute indirect light at 60Hz. This is an encouraging result! (Notice the red glow in the room, as the light drops.)

But it does leave me wanting for more... how can I do large scenes? With conventional, direct illumination rendering, you always have the option to split up your world in chunks, and treat them separately. Could I do something similar for Practica Boxel? What if I create a 3x3 grid around my avatar, and only compute indirect light every frame for the center sector? And compute the indirect light of distant sectors sparingly?

And there we hit the first snag: just because a chunk is remote, does not mean it cannot influence indirect light of the center sector. At the very least, we need to consider remote light sources too. If a lightsource is considered at one chunk but not at the next, a division will clearly show up.

Ok, the good news is that with the Photon Mapping approach I use, the use of many lightsources is very cheap. I even added a mechanism to prioritize lightsources, so that more photons are shot from nearby ones.

But there is more to it. Distant geometry also influences the light, either as an occluder of light, or a reflector of light And especially in my implementation, the cost of extra geometry is large! I opted for a linear algorithm that tests all geometry against a photon's path. This eliminates the dynamic branching that a spatial subdivision algorithm would bring, so the CUDA code can blast through it at full speed. This is great for small scenes, not so great for large scenes.

Geometry just beyond a sector's reach will fail to exert its influence on the lighting of objects nearby. I was half-way hoping that the error would be small enough... light falls off with the square of the distance, so should quickly go to zero. In practice, the human eye is just too sensitive to small discontinuities in light.

Six chairs. Spot the sector boundary! Only half of them receive light from the wall.

I guess one way around this, is to have two separate boundaries: the sector boundary itself, and a larger boundary that contains the geometry of influence. But no matter how you set the second one, there will be a distance that separates the inclusion and exclusion of an object, and it will show up.

The other approach would be to suck it up, and abandon real-time calculation of indirect light. Just photon-map the entire world as a whole. And no longer do immediate light calculation. The light would be static, or updated at 1Hz instead of 60Hz. But if it is static, why bother with all this, and not just use Blender or Maya to bake light maps?

Real Time Global Illumination seems so close, but... not quite in reach for complex scenes. To be continued.

Friday, February 12, 2021

On the allocation of photon-budgets for light sources.

I am making a real-time Photon Mapper, called Practica Boxel.

Photon Mapping is an algorithm that computes Global Illumination for a scene. This means that unlike traditional real-time rendering techniques, it does not merely compute the direct illumination, but it also considers light that is diffusely reflected from an object, onto another object. This is an incredibly expensive technique, since there are near infinite paths that a photon can take from a lightsource, into your eye. Hence, most GI images are either very slow to compute, or very noisy.

The Photon Mapping algorithm is O(N) in the number of photons (linear.) Or traditionally, even worse than that, as you cannot store a photon in a kD tree in O(1). But my underlying photon storage does have O(1), so in this case, the total algorithm stays at O(N) and thus scales linearly.

Henrik Wann Jensen, the author of the Photon Mapping algorithm, mentions that you can keep the photon count independent from the number of light sources. If you increase the number of light sources in your scene, you do not necessarily need to increase the number of photons that you shoot. So far so good.

But still, I would like to prioritize my light sources, so that the sources that influence the scene illumination the most, shoot the most photons. I need to allocate a photon budget for each light source.

And this brings me to an interesting problem to solve. If my camera roams freely in the world, I need to pick out which lightsources are closest to the camera. These sources are more likely to affect the illumination of the parts in the scene that are close to the camera.

My photon mapper has an additional requirement: for fast indexing, I need my light-source count to be a power of 2. So if there are 9 sources in the world, I actually need to feed my computational kernel 16 sources, some of which are copies. A source that is represented twice can simply be scaled down in brightness, so that both copies combine to the same power output.

It would be beneficial to have the light source furthest away from the original 9, be the one that is not duplicated, and shooting fewer photons (only half of the duplicated ones.)

But I don't have to stop at merely duplication. I could just create 256 light-sources out of the original 9, and do this in such a way that a very nearby light (e.g. a head-mounted light) could get many copies. Let's say 20 of them. And have the street lamp, all the way at the far end of the street be represented by just one. Once fired, those photons have to be scaled of course: even though the street lamp only shoots a few, those that it does shoot, have higher power, whereas our 20-fold duplicated head-lamp shoots much more faint photons. However, the count is really high, so that the noise up-close to the camera is lower.

So here we have my interesting problem:

  • Given N light sources L1..LN...
  • With distances to the camera D0..DN...
  • How to map them onto M (which is 2ᵏ) virtual sources LV1..LVM...
  • So that if source Q is F times further from the camera than source R...
  • It is mapped roughly F times less often onto the LV set than source R.
  • k Can be chosen freely, up to, let's say, 10.

This seems a non trivial problem to solve, so I am leaning towards a simplified version of it, where I classify each original source L as:

  • FARTHEST
  • FAR
  • MED
  • NEAR
And subsequently assign them to 1,2,3 or 4 virtual sources. After adding up the assignments, round up to a power of 2, and distribute the remainder T to the T nearest sources. Scale down the power of each virtual source depending on the representation count.

I'll report back once I have implemented this, and evaluated how it behaves under a roaming camera in a larger scene.

UPDATE

In the end, I went for a slightly different approach: allocate (k * distance) emitters for each lightsource. I do a binary search for a value of k that results in a total emitter count of 128. Done.