Skip to content

The letter

16th of Sun’s Dusk, 5E 138

Dear Colleague and Successor,

I am writing this letter to you on the last day before I resign my office and withdraw from mortal society.  It is my final gift to this place; one I pray will never be used.

I hope you followed my instructions carefully in breaking the seal and that your companions are all still alive.  I do not know what need pushed you to do this, but I hope for the sake of the College that it’s a dire one.

Within the chest that should now stand before you are many legendary artifacts, most thought to be lost and some that should never have been found.  There are also the journals and research notes of many famed mages and alchemists, as well as my own notes both on the artifacts and on various places and events I believe none but me have seen and lived.  I have no way of knowing who you are or what you may need, so I have given you all that I dare.

I would have tried to lock these things away forever if I had not myself once saved the College using an artifact I had no business even speaking of at the time when I wielded it.  You will find that one in the chest among many others.  Use them with the wisdom I hope was required of you to be granted this position and, gods willing, you will succeed.

Finally, in case the problems faced by the College is more mundane in nature, beneath these items there are three full sacks of gemstones.

As for myself, I cannot even imagine how time and tongue must have distorted my legacy, so here are the facts as I remember them:

I came to the College in the autumn of the year 4E 201; a young alchemist seeking knowledge and power.  Mostly power, I’m afraid.  After an accident left the College without an Arch-Mage only a year later, I was asked to replace him.  Young and foolish as I was, I accepted, but with the council of my dear late friend and colleague Master Wizard Tolfdir, I slowly grew into the role.

I was made a vampire soon after enrolling as a student.  Vampire royalty, no less, of a line native to Skyrim.  I chose this and I do not regret it, but I was utterly unprepared for the reality of this peculiar existence. No book can contain its horrors or its delights.

Duty pulled me back to the College, and duty and responsibility held me after my appointment, but with time I grew to love this place.  With time, I began to love knowledge for its own sake. This, above all else, I take with me in the nights to come.

Most of the current faculty is aware of my condition and I have even given lectures on the subject.  Only once did I lose control on these grounds, after I had been secluded in study for weeks and had forgotten to feed. The unfortunate Apprentice who came into my chambers did survive, but he was never the same again. For that, I continue to do penance.

I hope those of us who are able to control our nature are still welcome to come here and study despite my lapse in judgement.

As the centuries have past, however, I have grown weary of petty mortal politics; slaughter and strife over dust and fleeting moments of power.

During my term, I have tried to sway the Jarls from their prejudices against magic and its practitioners, but to no avail.  Most have no interest in knowledge other than as a tool to gain advantage and care little that their ancestors used to call it the Clever Craft and accord it great respect.

Even the College is afflicted by politics and my patience for the quibbles of children isn’t what it was, even for older children learned in magic and lore.  So at sunset tomorrow I will return to the court of my kindred.  I may even sleep for a while.

I leave this place and this office in the capable hands of Talmeni Maryon, our esteemed teacher of Destruction, and I have given her the instructions that lead here.  Brave Talmeni, flame of Colovia, I hope you are not the one to open this.

Do not attempt to find me.  I cannot guarantee your safety on that journey.

Nalcarya
Arch-Mage, College of Winterhold

On C++

Having spent a few years on Hacker News, I’ve seen the same set of arguments against C++ being a useful language being put forth time and time again. Having used C++ as a hobbyist since 1996 and professionally since 1998, I’m continually surprised by the claims made against it.

Here are some snarky responses to some equally silly arguments.

C++ takes a lot of work to learn

Yes, it does. Learning the proper use of highly complex tools is like that.

When I use C++ I keep using more and more features of the language until my code becomes incomprehensible to me

How, exactly, is that the fault of the language?

Everything you can do in C++ can be done in C

Everything you can do in C can be done in assembly language too.

Inexperienced programmers write bad C++ code

Inexperienced programmers write bad code in any language. Moreover, C++ is not a language for beginners.

Everyone knows a different 10% of C++

If you only know 10% of the language you work with, perhaps you should either learn the language properly or get a job coding in another language.

C++ is an ugly patchwork of concepts and ideas.

Yes, it sure is.

C++ has a lot of legacy garbage syntax

Yes, it sure does.

C++ is bad because it doesn’t have feature X

It’s refreshing to see someone who thinks the language isn’t large enough yet.

Quickie: Ocean longjump

As I said in the previous post, I couldn’t be bothered to add flying at this point, so instead I increased the jump impulse a hundredfold. I thought I’d post a screenshot from a few hundred metres up, in case anyone who read about the ocean shader was wondering whether or not it can handle that.

Oceans of fun

Two days ago I started on the ocean shader for Pod and another, smaller, single-player game project I will write about later. I’m currently doing most rendering work on the smaller code base, but the results are trivially shared as they both use the same engine. I thought it would be interesting to keep a work log of the development process, as a way to make a shiny and possibly even interesting blog entry. At the time, I had no idea how large this would become.

Note that I haven’t got started on proper sky rendering yet, so the skydome is simply mapped with a 1D gradient texture and there is no actual atmospheric scattering to apply to the ocean surface. This makes the horizon sharper than it should be under most circumstances. Please keep that in mind when looking at the screenshots below.

I use only a single 256×256 normal map for the wave normals throughout the process below and never sample it more than five times per pixel. However, I did replace the contents of it several times.

There is also only one simple directional light source that, for all images below except one, is set to 45° above the horizon.

My first attempt. Blinn-Phong on three summed samples from a normal map generated from regular Perlin noise. Tinted with a hard-coded blue diffuse colour. Hard-coded ambient factor. Horrific.

The same shader as above, but it turns out that Blinn-Phong half-vector interpolation is more sensitive to poor tessellation than regular Phong world position interpolation. Dividing the plane into a grid helped a lot. It still looks horrible, though.

Made a slightly better normal map. Moved to regular Phong. Started using the surface normal (no, not the wave normal from the normal map samples) to reflect into the skydome texture. Slightly less jarring but still awful and not really recognisable as water.

Improved the reflection calculation somewhat. It was still fairly broken at this point, however. Borrowed the wave height map from chapter 18 of GPU Gems 2 and made a normal map from that. Switched to a more subtle normal map filter, smoothing the surface out somewhat. It’s starting to look like a liquid, if perhaps not water.

Lowered the ambient and diffuse factors after some feedback from Pao, but otherwise mostly the same as above. This could maybe pass as being water if you squint and imagine you’re off the coast of a tropical island.

Replaced the ambient lighting with light absorbtion, by adding a slightly broken approximation of Fresnel and using it to blend between two absorbtion factors, with the closer one absorbing less green light. This makes it tend towards green closer to the camera. Modified the skydome colours using reference photos. This is the first one to be recognisable as ocean water.

Realised that water is shiny or transparent and threw out the diffuse part of the lighting. Replaced the borrowed normal map with one I made made in Gimp. Added a layer for tiny ripples, bringing the total number of normal map samples per pixel to four. At this point, I was fairly pleased with the results and headed off to bed.

Saturday. Being away from the computer for a while really helps one to find flaws in what had looked so good after having stared at it for hours on end.

Changed the Fresnel from my approximation of 1.0 - pow(dot(N, V), 2.0) to it’s current state of pow(1.0 - dot(N, V), 2.0) thanks to the paper Per Pixel Fresnel Term. Fixed a bug that was making the entire surface reflect the part of the sky just above the horizon. Started multiplying specular reflection by the Fresnel factor as well. Added another layer, this one for larger waves. This isn’t really visible from this close to the surface, but does help when viewing it from for example hilltops.

Note that the bottom screenshot of the set above uses a slightly different near absorbtion factor that lets out less green light.

At this point, which was yesterday afternoon, I thought it was looking pretty good. I declared the shader complete for now and began writing this blog post. However, as I was describing what was then the final stage, I realised how flawed it was. So instead of publishing, I went back to work.

Instead of interpolating between absorbtion factors, I should be interpolating between emission (from scattering) and partially absorbed reflection, i.e. looking straight down at water, it doesn’t reflect the sky. I also discovered that my R vector had been inverted, as I had forgotten to negate the light vector. Surprisingly, it had looked fairly good even with this mistake. I hadn’t noticed it as my terrain (not shown) still has an appalingly simplistic shader so there was no sane reference point.

However, after these (arguably correct) changes, the water looked like plastic. I then spent most of the night breaking things and then trying to figure out what had broken, why it broke, and how to fix it without going back to the previous, broken behaviour. In the process of doing this, I ended up reading about and figuring out a lot about the properties of water in general and the ocean in particular, as well as making minor digressions into other areas.

Surprisingly, as I learned each new things and applied it to the shader, it invariably got shorter and simpler, but when I went to bed last night it still didn’t look right.

Above is one example of breakage. In this case, among other bugs, the sun vector is… well, where no respectable sun vector should ever go. Note that the tearing towards the bottom of the image isn’t a rendering artifact but rather the result of gnome-screenshot interacting poorly with the screen refresh.

Sunday. Today I did finally manage to figure out what I had broken and how, and to match and arguably surpass the quality of the previous versions. While doing this, I also discovered that about half of the magic numbers used for scrolling were superfluous and took them out. Below is the end result. Enjoy.

In the image below, the contents of the top window is rendered using my shader while the window behind it is the photograph I used to create the skydome gradient. The ocean’s colours fall out of a combination of the reflection and emission factors, but even with such a simple model it matches reality fairly well.

Note that the reason that the waves seem to fade out at different heights within the image partially due to my screenshot being taken from eye level at a beach while the photograph is from a helicopter and partially due to the photograph having been murdered by compression.

Here is the shader in its current state, including the parameters used for the final set of screenhots. I haven’t made any real effort to optimise it yet, so I’m sure there’s room for that.

The variables wyTime and wyCameraPosition are shared uniforms provided by the engine, while sunDirection is provided by the game, using the same mechanism. Their values should be self-explanatory.

uniform sampler2D wavemap;
uniform sampler1D skymap;
 
uniform float exponent;
uniform vec3 emission;
uniform vec3 reflection;
 
varying vec2 texCoord;
varying vec3 worldPos;
 
const float PI = 3.1415926535;
const vec3 Y = vec3(0.0, 1.0, 0.0);
 
vec3 normal(vec2 coord, vec2 time)
{
  return texture2D(wavemap, coord + wyTime / time).xzy - vec3(0.5);
}
 
void main()
{
  vec3 N = vec3(0.0);
 
  // Idea stolen from Source 2007 (thanks!)
  vec2 texCoord2 = vec2(texCoord.x + texCoord.y, texCoord.y - texCoord.x);
 
  // Here be magic numbers.  Don't try to make too much sense of them; they're
  // not calculated from anything but rather the result of much tweaking.
  // There are two kinds:
  //  * The texture coordinate scaling, controlling wave size
  //  * The time scale, controlling wave speed (also influenced by wave size)
  N += normal(texCoord * 5.0, vec2(200.0, 166.0));
  N += normal(texCoord * 31.0, vec2(94.0, 70.0));
  N += normal(texCoord2 * 67.0, vec2(56.0, 34.0));
  N += normal(texCoord * 203.0, vec2(100.0, 20.0));
  N += normal(texCoord2 * 123.0, vec2(60.0, 57.0));
 
  N = normalize(N);
 
  vec3 V = normalize(wyCameraPosition - worldPos);
  vec3 R = reflect(-sunDirection, N);
 
  // This is far from accurate, but it still looks fairly convincing
  float fresnel = pow(1.0 - dot(N, V), 2.0);
 
  // We fake HDR sunlight for now, until the actual sky is operational
  vec3 fakeSun = vec3(7.0) * pow(clamp(dot(R, V), 0.0, 1.0), exponent);
  vec3 reflected = fakeSun + texture1D(skymap, 1.0 - dot(V, Y)).rgb;
 
  vec3 C = mix(emission, reflected * reflection, fresnel);
 
  gl_FragColor = vec4(C, fresnel);
}
<?xml version="1.0"?>
<material version="6">
 <technique type="forward" quality="1">
  <pass>
   <program path="shaders/sea.program">
    <uniform name="exponent" value="40.0" />
    <uniform name="emission" value="0.1 0.15 0.12" />
    <uniform name="reflection" value="0.65 0.65 0.65" />
    <sampler name="wavemap" texture="textures/wavemap.texture" />
    <sampler name="skymap" texture="textures/sky.texture" />
   </program>
  </pass>
 </technique>
</material>

Here is the skydome gradient.

Here is the wave normalmap.

So there it is; a usable deep ocean shader. Of course, I will need to add many more features to it for the games I’m making, at the very least including depth fogging, geometry reflection and the interaction of waves and land. However, first I need to let the land and the sky catch up visually.

A cup of fail

I haven’t posted anything in a while, so I thought I’d post screenshots of a bug from the move of cloud sprite vertex processing onto the GPU.

Broken ingame cloud rendering

Broken ingame cloud rendering