A Christmas Ghost Story

What do you do the night of Christmas Day? When all the presents have been unwrapped, the food eaten and the visits made? There's an old tradition, predating M.R. James and Charles Dickens, and even the author of Gawain and the Green Knight. I think we should bring it back: The telling of ghost stories.

Cantwell had never taken the time. Another version of her would have assumed that she knew what a ghost was. The present Cantwell was rarely the type to bother with abstractions. What a ghost was cost her no more concern than the question of what a friend was. Were either real? Her friends demonstrated fealty on the right apps and were present in person when circumstances required. This sat comfortably enough in place of a definition. Likewise were ghosts considered by some influential people (and what other kind existed–meaningfully existed?) to be a thing one could accept as “real.” Our alternate Cantwell would have said that a ghost was what remained when a person had otherwise died. They symbolized the inevitable loss of beauty and influence that preceeded the grave by so many years (for those who couldn’t contrive to go out on top) but were otherwise nothing more and much less than a person on this side of the ground. The thought of meeting a ghost hadn’t crossed Cantwell’s mind since she had been very, very small, and understood very, very little.

This Cantwell, the present Cantwell, got by with surface glosses in place of understanding. Understanding was a thing that lived in a stillness she simply didn’t inhabit. She felt naked without a constant crush of attention from all sides, like some deep sea chamber that would rupture if brought to the surface.

It came to pass, however, that she found herself in just such an unaccustomed stillness passing the canal opposite Christiansborg. Her devices were as silent as the unseen water below. Given their use during the day’s brief sunlit hours, this was not mysterious, though car headlights somewhere in view would have been more usual. The silence ate at her much more than the darkness and the cold. Thoughts echoed that didn’t feel like hers.

She was not precisely in her right mind, if one can ever be said to be. An unsettled mind is usually crosscrossed between past conditionals and possible futures, in Cantwell’s case none more than 48 hours in either direction, but hers was also occupied with several alternate presents where others had granted or withheld one thing or another.

Cantwell had a place and time to be, and was hating it as much as the remainder of the present void. The city could be any city of sufficient cachet to her; she didn’t speak the language and didn’t care to, for they spoke hers. Places were backdrops, set dressing. The bare black stage around her was growing intolerable. It was, in fact, the longest night of the year.

There was another, opposite her. A ghost.

Cantwell noticed her, and had the unacustomed jolt that noticing her was her first and most fatal possible mistake. In the way that one knows a greyness under a lamp is a human shape, and that a blank oval near its top is a face looking at us, Cantwell saw it out of the corner of her eye. She pretended, unconvincingly, that she hadn’t. Normally, she pretended so effortlessly and so totally that she herself believed it. Truth was, to Cantwell, what others would follow, and the strongest opening move in affecting a truth was to believe it herself. There were fallbacks, of course, on the vanishingly rare occasion of being trapped in a “lie:” crying, screaming, inversion… But belief could only take one so far. Her skills were deserting her, and would not save her.

Across the canal, behind the low railings, the figure matched Cantwell’s pace. There was a sound of footsteps on stone. She knew it was a woman, as she knew it was a ghost. The figure followed. The canal turned, and the figure didn’t. It forded the air at a calm walking pace, at an angle to meet Cantwell’s path. The silence echoed more loudly than the noise. Had it passed through the railing? Apparently. Even looking wouldn’t tell her, and Cantwell was absolutely not going to look. In another context, it could have been a school friend or a colleage from some job quickening step from across a street to trade commonplace words. Here, however, nothing could be commonplace. The grey-black mass took up more and more of Cantwell’s peripheral vision. A second set of footsteps began on the cobbles to her right, matching rhythm. The ghost walked along beside her.

“Aren’t you going to-“

“No!” Cantwell snapped, equally surprised to hear her own voice.

“You can’t know how much I hate you,” remarked the ghost, also not making eye contact. Cantwell hustled on, saying nothing. People, other people, would save her. Her silence, far from rallying strength like usual (stillness could also be used offensively as a weapon) resulted only in a gently lengthening sense that she was making herself an object of pity, drawing out the inevitable.

“I don’t need this from you,” said Cantwell, eyes set straight ahead. Her piteousness rose to something like self flaggelation.

“Tonight’s not about what you want. Tonight is about what I want.”

What was this? Was she going to be hurled–hurl herself, but not really–into the canal? Float to be found at late morning light an ugly corpse? Self-killed (so it would appear) without a mark of respectable violence?

“No,” said the ghost.

“What do you want?”

Nothing happened. She wasn’t transported, or overwhelmed with a sudden hallucination. To be truly overwhelmed with something is a rare gift in life, and this was not the night for gifts. Cantwell was no less aware of the cold air up her skirt or the trouble of negotiating each increasingly slick paving stone in her high boots. It was as if a smell from long ago triggered a sudden memory. Cantwell’s emotions were once again in a tiny room overlooking another city. Nana was baking macaroons. Some were red, some were white, and some were yellow. A bowl of blue batter remained. She had done nothing to help, just sat at the table kicking her feet and eating. It didn’t matter. The halo of something was in the air. Little her didn’t understand why everything was good, and didn’t care. Why would a child?

“Shut up!” yelled the ghost. Cantwell was startled, and almost looked over. There was no one else there, but the ghost didn’t seem to be addressing her.

She saw a window. A small, far off, lighted window, on a third floor, looking warm as the finished wood inside, all of it seeming to glow. Was this the only lighted window in view? The last one in the world? “I don’t understand,” she began, but was cut off again.

“Never! Ikke nu, ikke hver,” continued the ghost, to whom- or to whatever. Cantwell seemed to be momentarily forgotten.

“You’re not real. It isn’t real.”  Tears pricked at Cantwell’s eyes. They served no purpose. They weren’t going to move the ghost. There was no one else to help–that much was increasingly clear. Cantwell wanted to control them, but with a dropping feeling found that she absolutely couldn’t.

She still hadn’t looked at the ghost. She wouldn’t. It was the only fight she hadn’t lost. The figure seemed in appearance about her age. A woman. Dressed in something colorless, perhaps warmer, or maybe older. It could have been her doppelganger. It could have been anyone else.

“Wouldn’t you like to know where I’ve been, before I was here?” They walked on in silence for a moment. “I wasn’t an ugly corpse. I know how that matters to you.”

Cantwell couldn’t form the words, but the ghost did for her:

“What do I know about you? No, this isn’t your night for questions.” It scratched its nose. “Not even the rhetorical kind. You tell me: Why were you at your Nana’s?”

“Mom was ripping the apartment up.”

“Right.”

“She was all cut up about some man cheating on her.”

“Her moods are extreme.”

“He wasn’t even my father. It’s not like I cared.”

“I’m taking that away from you.”

“That man? I barely even remember him.” Cantwell stopped herself. It wasn’t that memory the ghost was taking. It wasn’t any memory the ghost was taking. Worst of all, it wasn’t her life either.

“What am I?” the ghost asked.

“You’re a ghost. You’re just a ghost.”

“And what is a ghost?”

“Don’t do this.”

“I thought you didn’t try to understand things. Just the surface, remember? Stay in the flow. It’s the silence that scares you. I’ll bet right about now you’re wishing you were stupider. There are things you don’t understand, but then there are things you can’t understand. That’s what I am. That’s what your ghost is.”

“Please!” Cantwell looked, but there was nothing there. “Please!” The memory was just as fresh. The moments in that kitchen. True to her word, the ghost hadn’t taken the memory. Cantwell could remember every detail with painful accuracy. Only the feeling was gone.

At some point it had begun to snow. Cantwell continued on to her appointment, in that wooden room on the third floor.

Solus: 2.5D Character Control & Footprints

The protagonist (we never came up with a name for her) moves along a 2D plane in a 3D environment, with generally realistic platforming movement inspired by Flashback: The Quest For Identity.  The system uses the Unity physics engine, manually controlling the character’s momentum to create grabbing and climbing, and adds quadratic drag for “crunchier” falling per Bennet Foddy’s 2015 GDC lecture. I started by modifying an existing character control script, the final system ended up a complete rewrite.

Character interaction is controlled with Layers. If an object has a Collider and is in Layer “Walkable,” the protagonist can traverse it, including ledge grabbing when appropriate. Rope climbing is the same, only with Layer “ClimbableRope.” (Wall climbing was also implemented, but cut for time.)

Want to play with it? You can download the Unity package here. Feel free to use the controller scripts & prefab setup for whatever you’d like (but not Anastasia Jacobsen’s cute character model please!)

Footprints are based on the method used in Röki. At the animation frames of the walking and running cycles where the foot first makes contact with the ground, an animation event is called with a boolean indicating left or right foot. A Projector Prefab with a Normal Map Texture is then instantiated at the location of the foot’s Bone. The Prefab has its own script, which fades the Normal Map out over 10 seconds, and then self-deletes.

The Solus demo is available on to download and play on Itch.io (Mac & Windows).

Solus: Lighting Up the Desert

Anastasia Jacobsen’s concept for Solus is an attempt at a semi-hard-sci-fi take on Alex McDowell’s “Planet JUNK” collaboration. The Earth has somehow stopped rotating, creating a 6 month summer/winter cycle and migrating the oceans away from the equator.

Logo art by Anastasia Jacobsen

In the demo, the player journeys down into the sand-buried remains of a skyscraper looking for water. For visual interest (and irony) I suggested the Futurist city of Brasilia which went over well with the team: Niek Meffert, Anastasia Jacobsen, Rosa Friholm, Ida Lilja, and myself. I was Technical Artist and Lighting Designer. (Solus was the first of two Planet JUNK collaborations. Many lessons learned were later applied to Shrooms.)

Solus uses Unity’s High Definition Rendering Pipeline (HDRI), allowing a wide variety of realistic volumetric effects—the simulation of light’s interaction with microscopic particles suspended in air, like smoke, water droplets and dust.

Desert scenes may never escape from Journey’s long shadow…

Topside, the lighting is very simple. There’s a Directional Light (sun) and not much else. Fill lighting is created by Global Illumination from the skybox. Blowing sand is created with the Unity VFX Graph. A number of post-processing effects are added, including Bloom, Tonemapping, Color Curve adjustments (for a more cinematic “desert” look) and a custom sparkle shader in the brightest areas. A faint volumetric Fog pervades the scene, to create a dusty atmosphere. Slightly behind the main plane of action, a second “thicker” Fog Volume is added, faded from bottom to top, to make the background distances appear greater and create a Bryce-like height fog effect.

Thank you, anonymous graffito

The underground lighting is primarily driven by a Point Light attached to the character’s lantern. The Volumetric Fog is thicker, increasing with depth into the buried skyscraper. An extremely bright Spot Light shines in through the entrance, volumetric and colored bright blue to contrast with the warmer lantern light. A similar, very narrow bright blue Spot Light shines down from the top of the first elevator shaft, as if a tiny stab of sunlight were blazing in through a chink in the roof. Farther down, mushrooms glow with an eerie green Emissive Material, casting light onto their surroundings via covert green Area Lights.

The theatrical darkness demanded that a final Light be added, to only be activated while editing the scene—literally named “Work Light.”

The Solus demo is available on to download and play on Itch.io (Mac & Windows).

Shrooms: HDRP in URP

The Shrooms demo runs on Unity’s mobile-friendly Universal Render Pipeline (URP), which doesn’t support volumetric fog and lighting like the High Definition Rendering Pipeline (HDRP). An early design decision was to lock the camera to only about 20 degrees of rotation off the default view axis. This allows many computationally-inexpensive (oldschool) cheats and tricks to create rich atmosphere. My mantra was: “HDRP in URP.”

Lighting

 In the Shrooms world, lightbulb is a job. Every light source is a glowing, bioluminescent mushroom person. The Copenhagen-inspired strings of street lamps that draw the viewer through the level each contain an animated Bulb Guy (created by Niek Meffert) sitting in a little wire gondola underneath a beat-up reflector. It’s a living.

He/she, and the remainder of the lamp, are set to not cast shadows, and contain a downward-facing  Spot Light. There are 37 in all, in addition to a wan Directional Light sun from the left—which is a problem, because Unity’s URP has a hard limit of 8 lights per mesh. The Unity Terrain tool splits the ground into a couple dozen smaller tiles, but the initial result was most of the light sources being simply ignored by the ground mesh, and glows often visibly sliced off where they crossed tile boundaries. Baked Lightmaps and realtime lighting in URP both share the lights-per-mesh limit.

Quick & dirty normal map in Photoshop: Filter > Other > High Pass, Filter > 3D > Generate Normal Map

The solution was to place pieces of flattened human-world junk along the ground, to disguise the boundaries and ensure that every light creates a visible effect. The junk shader uses a Texture stitched together in Photoshop from derelict building photographs, with a rough Normal Map.

Like the noise functions, the Texture is applied in World Space, allowing the same low-res crumpled square of debris to be recycled, stretched and resized ad-nauseum, with the Texture remaining undistorted and matching up perfectly at object boundaries. I’ve been a big fan of using world space shaders to create visual variety in instanced models since The House of Time–which, yes, will finally get some big updates this summer.

Simple exponential-squared Distance Fog ties the effects together, creating additional depth, and a Bloom post effect softens the edges of windows and other bright objects to match. A Depth of Field post effect further softens objects in the extreme foreground, adding to the murky intimacy, and the deep background is a hand-painted backdrop by Natasha Beck in an Unlit Shader.

HDRP in URP: A mix of simple, oldschool tricks and modern GPU-driven effects.

Faking Volumetrics

 It’s a not-so-dirty not-so-secret that even in high-end film compositing software volumetric lighting is faked by slicing the camera’s Z-axis into stacked, transparent planes at render time. This is what Shrooms does manually. Using the limited camera view and careful placement, patches of fog are created with a shader on a small stack of transparent planes. The shader multiplies a half-circle gradient alpha Texture with a procedural noise function. The noise slowly migrates up the Y-axis, as if mist were rising off the swamp. The noise is generated in World Space, so that scaling, squashing or stretching the fog planes creates no distortion to the noise pattern.

Light glows work the same way. Each light fixture model contains a set of three  planes: Two larger, colored, more transparent ones in front and back, and a smaller, more opaque, white plane in the center. The alpha Texture is a narrow cone gradient, aimed downward, and the World Space noise function slowly falls, like misty drizzle. The bright spotlights in the arena and cafe are just variants on this scheme, and a circular glow is used in a couple of additional spots.

Shrooms: Color & Forms

In Niek Meffert’s concept for Shrooms, giant mushroom people battle giant plant people in their swampy homeland, while grinding the remnants of humanity under their figurative boots. The dev team was Meffert, Lucas Oliveira, Sabrina Christiansen, Kaspar Dahl, Natasha Beck, and myself as Lighting Designer and Technical Artist. You can check out the demo (Mac & Windows) on Itch.io here.

Frequently heard during environmental modeling: “It’s good, Sabby. Get rid of the straight lines.”

The objective was to create a bright, colorful, murky, fungal setting. Fungus suggests bright, “sickly-sweet” tertiary colors, and we wanted an organic, lively scene. However, with too much clashing color the scene would have become busy and unreadable. Just finding your way and knowing what to interact with would have meant a frustrating cognitive load.

For that reason, I worked with the team to enforced certain rules to control user attention. The main character is in complementary colors. The bad guy’s color palette is a high-saturation split complement. NPC characters each have a single, dominant color. Non-interactive parts of the scene favor muted, analogous colors.

Lighting rules were also held to. Unimportant parts of the level fall back into mist and shadow. The character path is comparatively well lit, always suggesting where the player can and can’t go. Interactive parts of the scene (usually just-for-fun destructibles) pop comparatively, while others harmonize.

Forms avoid straight lines, with blobby, asymmetrical and impractical shapes but—importantly—recognizable outlines. Classic Warcraft games, and the art of Chris Sanders (Lilo & Stitch) were strong references here.

And of course, what’s the point of a game without asshole physics?

Oh Steam…

Kubrick twist: Searching Steam aliases is notoriously broken! This seems to be a known issue that’s been causing consternation for half a decade. Specifically, aliases with trailing numbers (e.g. spacetoast123) are unfindable with the exact string, and usually with a partial string.

So, while I’ve been able to send a friend request to the Second Prize raffle winner ($25 USD), who has no trailing numbers, nothing I’ve tried has brought up profiles for the First and Third Prize winners ($75 and $25, respectively).

If you entered the raffle, and your Steam alias ends in numbers, please friend me via my public profile page: https://steamcommunity.com/id/rhinocrate/ Over 300 hours of Civilization VI? Yes, it’s a problem. Regardless, you may have already won! I promised not to post handles publicly (and creating a duplicate with the same alias seems quite easy) so winners can’t be directly announced.

Bottom line: If you ever run a stunt like this yourself, even though it might make you look spammy, get an email too.

Raffle Code

As promised, I’ll be raffling off one $75USD Steam digital gift cards, and two $25 cards shortly. Just to be super transparent, here’s the raffle code.

And no, I’m sure mathematically this wouldn’t hold up in a real lottery–but for a raffle with fewer than 20 valid entrants, I think we can call it good.

function rankRaffleEntrants(){
        var entrants = "Prima Secunda Tertia Quarta Quinta Sexta Septima Octavia Nina Decima"; //Steam handles
        entrants = entrants.split(" ");
        ol = document.getElementById("orderedList");
        //print function
        function addToList(entrant){
          var li = document.createElement('li');
          var nextInList = document.createTextNode(entrant);
          li.appendChild(nextInList);
          ol.appendChild(li);
        }
        while (entrants.length>1){
          var pick = Math.round(Math.random()*(entrants.length-1));
          var entrant = entrants.splice(pick,1);
          addToList(entrant);
        }
        addToList(entrants[0]);
      }