Day 22 - 🔬 Deep Dive - How IBL Works

General / 01 June 2026

Image based Lighting turns a photograph of the world into a light source. Here's what actually happens between the cubemap and the final pixel.

Why Lighting From a Photo Works

Ambient lighting gives surfaces a uniform base color from all directions equally, a practical shortcut, but an obviously unrealistic one. In the real world, light arrives with wildly different intensities depending on where you look.

Image-based lighting replaces that flat approximation with a photograph of the actual environment. Every texel of the captured image functions as a light source, so instead of placing discrete lights by hand, the scene is illuminated by the world around it. Think of a VFX shot where a CGI element needs to match on-set footage, and IBL is what makes that integration feel effortless. For games it serves the same purpose: a quick way to establish a grounded, photoreal read without hand-placing dozens of lights.

That's what makes IBL feel different from traditional lighting setups. The environment contributes from every direction simultaneously, which is why IBL-lit scenes look grounded rather than staged.


source: marmoset

There are some limitations worth keeping in mind. Cubemaps for IBL need to be captured as HDR - typically 32-bit per channel EXR (FP32) or 16-bit half-float. Even then, extreme dynamic ranges can be a problem. A sunset sky with a visible sun is a common example: the contrast is high, and the capture process itself - cameras and HDR merge workflows - can't reliably capture the full range when the sun is in frame. The sun's luminance at noon is approximately 1,600,000,000 cd/m², which FP32 can store numerically, but getting there accurately from a real-world capture is the hard part.

For more info on capturing real life IBL footage: https://marmoset.co/posts/hdr-panorama-photography/

Building the Cubemap

The environment needs to be recorded as a spherical function - incoming radiance from every direction around a point. That data gets stored in one or more images, indexed by direction. This is environment mapping. It's one of the most powerful forms of environment lighting: more memory-intensive than other spherical representations, but simple and fast to decode at runtime. Critically, it can represent arbitrarily high-frequency signals just by increasing resolution, and capture any range of radiance by increasing bit depth - which is why HDR capture matters. A standard 8-bit texture would clip the sky and produce incorrect lighting integrals.

Several projection formats exist, each with different trade-offs.

Latitude-longitude (equirectangular) is the dominant exchange format. The reflected view vector is converted to spherical coordinates - longitude (azimuthal angle, 0 to 2π) and latitude (polar angle, 0 to π) - and mapped to UV:

Distortion is unavoidable when projecting a sphere onto a rectangle. The bigger problem is that the sample density is highly non-uniform: the poles are oversampled relative to the equator.

Sphere mapping derives the texture from the appearance of the environment as viewed orthographically in a perfectly reflective sphere. Photographing a chrome ball is the classic real-world capture method - the result is called a light probe. The mapping is view-dependent though, which limits its use, and a reflective sphere only captures the front hemisphere. In practice, sphere maps are typically assumed to operate in view space.

Cube mapping is the runtime standard. The environment is projected onto the six faces of a cube centered at the capture point, stored as six square textures with no wasted space. Synthetically, they're produced by rendering the scene six times with a 90° FOV; for real environments, equirectangular panoramas are reprojected. The key advantages are that it's view-independent, hardware-native, and indexing is trivial - the reflected view vector r is passed directly as a three-component texture coordinate, no trigonometry needed, and r doesn't even need to be normalized.

Dual paraboloid mapping uses two textures, each covering one hemisphere via a parabolic projection. Octahedral mapping unfolds the sphere by cutting eight triangular faces and arranging them on a square or rectangle. It avoids the filtering artifacts that affect some other projections, and the L₁-normalized lookup is straightforward to implement in a shader.

Splitting Diffuse and Specular

The full IBL integral (incoming radiance × BRDF × cosine term over the hemisphere) is too expensive to evaluate per pixel. The solution is to precompute as much as possible, exploiting one key observation: diffuse and specular respond to the environment at very different frequencies.

Diffuse is low-frequency: only the general hemisphere energy matters, not the exact incoming direction. Specular is high-frequency and view-dependent: smooth surfaces pick out a narrow reflection cone; rough surfaces blur it.

Diffuse - irradiance and Spherical-harmonics

The diffuse component integrates incoming radiance convolved with a cosine-weighted kernel. The result is very smooth, which makes it ideal for compression. Spherical harmonics (SH) are the natural fit.

SH are an orthonormal basis defined on the sphere. Projecting the irradiance function onto each basis function gives a coefficient capturing how much of that frequency is present. The first three bands (L0 through L2, nine coefficients total) reconstruct diffuse lighting with very little error.

A key property is that the integral of the product of two functions equals the dot product of their coefficient vectors:

This means the diffuse irradiance integral reduces to a dot product at runtime, which is essentially free. Low-band SH avoids ringing (Gibbs phenomenon) because diffuse lighting is inherently smooth and well-represented by just a few coefficients.

Specular - prefiltered environment map

Specular is both roughness-dependent and view-dependent. A perfectly smooth surface samples a single reflected direction; increasing roughness blurs the reflection over a wider cone. This is represented as a mip chain of the environment map, where each level stores the environment pre-convolved at a different roughness value. Rough materials sample higher mips; mirror-like materials sample the base level.

The BRDF Integration Map

The split-sum approximation factors the specular integral into two independent parts. The prefiltered environment map handles the lighting side. The other part captures how the BRDF itself responds to a given roughness and viewing angle - independent of what the environment looks like.

This is baked into a 2D LUT, indexed by NdotV (cosine of the angle between surface normal and view direction) and roughness. The LUT stores two values per texel: a scale and a bias that together parameterize the Fresnel term. Because they're computed under a fixed white environment, they're analytically correct for any real environment when combined with the prefiltered map.

The LUT is computed once, offline. At runtime, it's a single texture lookup - (roughness, NdotV) returns (scale, bias) - and a couple of multiply-adds. No per-pixel integration required.

From Cubemap to Final Pixel

At render time, three lookups reconstruct the full IBL response:

  1. Diffuse - evaluate the SH coefficients with the surface normal (a dot product), or sample a pre-convolved cubemap. This gives the irradiance for the diffuse term.
  2. Specular lighting - sample the prefiltered environment map at the mip level corresponding to the material's roughness. High roughness → higher mip → blurrier reflection.
  3. Specular BRDF - sample the 2D LUT at (NdotV, roughness) to retrieve the scale and bias.


Practical Takeaway

The mip levels of a prefiltered environment map correspond directly to roughness. A roughness of 0 samples the sharpest mip; 1.0 samples the most blurred. That means roughness map quality shows up directly in the IBL response; banding or precision issues in the texture will produce visible artifacts in the reflection.

IBL also assumes PBR-compliant materials. The split-sum derivation is built on the same energy conservation and Fresnel assumptions that PBR materials follow, so the two are designed to work together, and a non-compliant material will produce incorrect results even with a perfectly captured environment.

Static IBL does have one hard constraint: it can't support a dynamic time-of-day cycle. Studios that need that reach for procedural atmospheric systems instead, giving artists direct control over how the sky and lighting evolve over time.

The shader combines them: diffuse albedo × irradiance, added to (BRDF scale × specular color + BRDF bias) × prefiltered radiance. Each of those inputs was precomputed. What the shader actually executes is three texture fetches and a few arithmetic operations - not a hemisphere integral.

Š 2026 Stefan Groenewoud - All views are my own, not those of my employer.

Day 21 - 🔬 Deep dive - PBR Compliant Work

General / 29 May 2026

Day 13 told you what broken PBR looks like. This post explains why the rules exist in the first place.


PBR-Compliant vs PBR-Using

When authoring shaders and materials, a PBR ruleset ensures that whatever goes into the shader is read out correctly by the lighting system. But the system, the BRDF shading model, also needs to be physically based. Otherwise whatever energy a material carries could be assigned or read out incorrectly, even with the right shader in place.

This distinction is crucial: having a PBR shader doesn’t guarantee physically accurate results. The artwork it processes must adhere to the same rules. A metalness value of 0.5 applied to a physically based BRDF still produces an impossible result, the shader can’t rectify the texture’s error. This is the framework for everything that follows. The sections below delve into the implications, beginning with the physics that necessitates these rules.

Metallic range from 0 to 1.


Energy Conservation

The unifying principle behind all PBR rules: a surface cannot reflect more light than it receives. Diffuse and specular are complementary, as one goes up, the other comes down. This constraint is what makes PBR self-consistent and is the reason the albedo range, metalness binary, and F0 values are what they are.

Whenever a material receives light and scatters it, energy is lost. A bright material like snow scatters more light than a dark material like charcoal, not further, but more of it. This doesn't just affect the material itself; it propagates through the scene via GI light bounces, which is why a non-conserving material can throw off the entire lighting environment around it.


F0 - Reflectance at Normal Incidence

A surface is the interface between the surrounding medium (typically air, with a refractive index of approximately 1.0) and the object's substance. When light hits that interface, the Fresnel equations describe how it splits: some reflects, the rest refracts and enters the material. The proportion that reflects is the Fresnel reflectance F, and it varies with the angle of incidence.

At normal incidence (light arriving perpendicular to the surface at θ = 0°) reflectance reaches its minimum. This baseline value is F0, and it's a fixed property of the material. What makes it useful is how predictable it is: almost every dielectric lands near 0.04 (4%), with a realistic range of roughly 2–6%. Water, plastic, skin, wood, glass: visually very different materials that all sit in roughly the same place. Their differences come from their diffuse response, not their specular.

As the angle increases toward grazing (θ → 90°), F climbs toward 1.0 for all materials and all frequencies. That's the physics behind the edge brightening visible on almost every surface; at grazing angles, everything becomes a mirror.

Metals are the exception. Their F0 is high (typically 0.5 to 1.0) and tinted across the visible spectrum. A gold surface reflects red and green more than blue, giving its reflections their characteristic color. For metals, F0 is the specular color, which is why a PBR workflow stores that color in the albedo map for metallic materials.

This is the physics behind the metalness binary rule. A value of 0.5 describes no real substance; real materials are either dielectrics (F0 ≈ 0.04, uncolored) or metals (F0 high, tinted). The gap between those two ranges is where physically impossible materials live.

The Cook-Torrance BRDF

The Cook-Torrance model splits the surface response into diffuse and specular. The diffuse term (typically a Lambertian or Burley approximation) handles light that enters the material, scatters internally, and exits in a random direction. The specular term handles light that reflects at the surface without entering, and this is where the physical complexity lives.

The specular lobe is the product of three terms.


The Normal Distribution Function (GGX is the standard implementation) describes the statistical distribution of microfacet orientations: microscopic surface irregularities that each reflect light like tiny mirrors. Roughness is the parameter feeding this distribution. A low roughness concentrates the NDF into a tight peak, producing a sharp highlight. A high roughness spreads it wide. This is why a flat grey roughness map reads as artificial: it collapses the NDF to a single value across the entire surface, something no real material does.

The Geometry/Masking term (Schlick's approximation is common) accounts for microfacets that shadow or obscure each other at grazing angles. Without it, the specular response would be unrealistically bright when a surface is viewed or lit obliquely.

The Fresnel term ties directly to F0. Reflectance isn't constant; it scales from F0 at normal incidence up toward 1.0 at grazing angles. The Fresnel term drives this within the shader, so a surface automatically brightens at glancing angles regardless of material type. At perpendicular incidence you see F0; at grazing you see full reflectance. This is what makes PBR materials behave correctly across changing view angles without manual adjustment.

Together, the three terms produce a specular lobe that is energy-conserving by construction: it distributes incoming light, it doesn't create any.


In-Engine Validation

The albedo range and the light test are covered in Day 13 - ⚡️ Quick - How to Spot Broken Materials, this section focuses on what those checks miss, and where the BRDF debug views pick up the slack.

Having ways to do custom Metal or Roughness override debug view can help isolate the specular response and expose metalness values that aren't binary. What a metalness override at 0.5 actually looks like vs 0 or 1, the wrong specular tint is readable immediately.

Roughness override: how a flat grey roughness map collapses the NDF to a single value and makes that obvious. Why these views catch compliance breaks that the albedo overlay doesn't see, you can have a correct albedo and still have a broken specular response. Also: the difference between checking under a neutral lighting vs a directional light for catching roughness issues specifically.


Practical Takeaway

The F0 values are the most practically useful thing to take from this post. Every dielectric sits near 0.04, you don't need to guess or measure. If you're authoring plastic, stone, or skin, the specular sits around 4% and the albedo carries the diffuse color. For metals, F0 is the specular color, so the characteristic tint goes into the albedo map and metalness goes to 1.0: not 0.9, not 0.5, not somewhere in between.

Violating energy conservation has consequences that extend beyond the material itself. A surface that reflects more light than it receives injects extra energy into every GI bounce in the scene, brightening indirect light in ways that are hard to track down. The error doesn't stay local.

The rules in PBR aren't arbitrary restrictions. They're the conditions under which the lighting math, the BRDF, the IBL precomputation, the tone mapping downstream, all produce predictable results. Stay within the ranges and the system behaves correctly. Step outside them and the shader can't correct for it.

Š 2026 Stefan Groenewoud - All views are my own, not those of my employer.

Day 20 - 💬 Take - The Lighting Skill Gap in Technical Art

General / 28 May 2026

Technical artists tend to think about lighting as a performance problem. It's also a storytelling tool, and that half gets overlooked.

The title might be a slight overstatement. I've only spent brief stretches of my career fully invested in doing lighting. But those periods taught me a lot about how lighting affects everything downstream, post-processing, bloom, exposure, color grading. I also used photography as a stepping stone: understanding how light controls mood and tone, and how you can use it deliberately to direct the eye or reinforce a narrative.

A well-lit scene leads attention. A poorly lit one lets it wander.

source: Resident Evil 7

The Technical Artist's Default

Most technical artists approach lighting primarily as a performance concern. How many dynamic lights are in range? Can we bake this? Are the shadow maps eating the budget? That instinct isn't wrong, lighting can be extremely expensive, and keeping it under control matters.

But the instinct can tip too far. I've seen lighting stripped back to the point where it sacrifices the intended look entirely. There's always a balance to strike between performance and artistic intent, and defaulting too hard to performance loses the plot.

The Student Blind Spot

I see a different version of the same gap with students. They'll spend weeks on a material or a model, getting every detail right, and then present it flat-lit in a gray viewport. The work can't speak for itself in that context.

Working on an asset means wanting to present it as well as possible. Knowing where to add or pull light to emphasize the storytelling matters. In a game environment, not every asset can be a hero prop, and lighting, just as in a film production, can hide a lack of detail where it doesn't matter, and draw attention to where it does.

A simple example: if you're building a horror environment, you don't fill the darkest corners with dense detail. With no light reaching those areas, the player won't see it. Understanding that is set dressing; it's also lighting.

A Note on Deferred vs Forward

Lighting in deferred rendering is generally cheaper per light than in forward rendering, though both have trade-offs.

In forward rendering, each light evaluates its contribution per-fragment, for every piece of geometry within its range, so the cost scales with the number of lights multiplied by the geometry they touch. In deferred rendering, geometry is rendered once to a G-buffer (storing normals, albedo, roughness, depth, and so on), and then lighting is evaluated in screen-space per pixel against the G-buffer data. Lights only operate on what's visible on screen, not on geometry, which makes adding more lights significantly cheaper.

That said, neither system is free. Not every light should cast dynamic shadows, shadow maps have resolution budgets and runtime cost, and using them indiscriminately is how a scene's frame budget disappears. Knowing which lights earn a dynamic shadow and which don't is part of the skill.

Source: learnopengl.com

The Broader Point

Having a basic understanding of how lighting works, how it interacts with surfaces, how it controls mood, and what it costs, is useful regardless of your specific discipline. Whether you're a material artist trying to present your work convincingly, or a technical artist optimizing a pipeline, lighting is part of the picture.

It's one of those areas where a little knowledge goes a long way.

Š 2026 Stefan Groenewoud, All views are my own, not those of my employer.

Day 19 - ⚡️ Quick - AO and Bent Normals

General / 27 May 2026

Ambient occlusion tells you how much light is blocked. Bent normals tell you which direction it comes from. One number vs one direction, and that difference matters more than it sounds. Again, this started out as a short format but ended up going longer than expected.

What AO Does

Ambient occlusion is a scalar value, a single number per point on a surface that describes how much of the surrounding hemisphere is blocked by nearby geometry. A value of 1 means fully unoccluded, fully exposed to incoming light from every direction. A value of 0 means fully occluded, buried inside geometry with nothing reaching it.

In practice AO is used to darken areas where light has less access, cavities, corners, the underside of a ledge, the gap between two surfaces pressing together. It approximates the soft shadowing that fills those areas in reality, and it does so cheaply because it is baked and static, which also means it can't update for animated or dynamic geometry.

The limitation is that it's directionless. AO darkens a surface based on how occluded it is, but it has no information about where the unblocked light is actually coming from. When combined with IBL, where the incoming radiance varies across the hemisphere - this becomes a problem.

Source: scahilldesign.co.uk


The Gap AO Leaves

When you sample a prefiltered environment map for diffuse lighting, you're integrating incoming radiance across the hemisphere above the surface. AO scales the result down in occluded areas, but it scales it uniformly, as if every direction contributed equally.

In reality, the sky above a surface is bright and the ground below it is dark. A surface sitting in a corner is mostly occluded from above, so it should receive less skylight, but it might still have a clear view toward a bright part of the environment. Scalar AO can't capture this. It just darkens without knowing what it's blocking.

Examples of different real-time AO implementations in Unity.


What Bent Normals Add

A bent normal stores the average unoccluded direction at a given point, the mean direction the surface can actually receive light from, given its local geometry. Instead of a scalar, it's a vector.

When sampling an environment map for indirect lighting, you use the bent normal to look up the right region of the map rather than the geometric normal. The result is that occluded surfaces sample from the part of the environment actually visible to them, not an incorrect average. A surface in a corner looking up at the sky samples the sky. A surface buried in a crevice samples the dimmer environment around it.

The improvement is most visible in complex indoor environments and on characters, where AO-only solutions tend to over-darken flat areas and under-darken the right spots.

Specular Occlusion

On every project I've worked on, specular occlusion has been worth applying. The problem shows up in deep cavities: the reflected view ray doesn't know whether the surrounding geometry is occluded or not, it just looks up a texel from the environment cubemap and reflects it toward the viewer. In heavily occluded areas, this produces light leaks. The surface looks like it's receiving specular light from directions the geometry would never actually allow.

The fix is to use the AO map to attenuate the specular result, multiplying the deepest range of the occlusion map against the specular values to dim or suppress reflections where they're least plausible. You don't need to apply this uniformly; a remapped range that only affects the heavily occluded end keeps the effect targeted. Ideally this is done as a post-process pass rather than per-material, so you pay the cost once rather than per shader.

Bent Normal

Specular Occlusion

More info regarding Specular Occlusion: https://dev.epicgames.com/documentation/unreal-engine/bent-normal-maps-in-unreal-engine?lang=en-US

Practical Takeaway

Bent normals are a baked asset, generated in the same pass as AO. The cost at runtime is minimal, you're replacing one texture lookup (geometric normal) with another (bent normal). In Unreal Engine, bent normals plug directly into the material and are used automatically for indirect lighting when present.

If you're already baking AO, baking bent normals alongside it costs almost nothing and noticeably improves how surfaces read under Image Based Lighting, particularly in areas with strong directional variation in the environment.

I did not discuss the other implementations that are available regarding realtime ambient occlusion ways of generating this information.

Š 2026 Stefan Groenewoud - All views are my own, not those of my employer.

Day 18 - 🔬 Deep Dive - Linear Luminance

General / 26 May 2026

Light doesn't behave the way your eyes tell you it does. Understanding the difference is what separates renders that look lit from renders that look real.

What Is Luminance

Luminance is the amount of light emitted or reflected from a surface per unit area, in a given direction. It is a physical quantity, measured in candelas per square-meter (cd/m²), and it describes what is actually happening with the light, not how a person perceives it.

Brightness is different. Brightness is a perceptual response, how the visual system interprets incoming luminance. Two surfaces with the same luminance can look different in brightness depending on context, surrounding values, and adaptation state.

The distinction matters in rendering because the math works with luminance, but the artist is looking at perceived brightness. Confusing the two is one of the most common reasons lighting setups feel physically off even when the numbers seem right.

How the Eye Perceives Light

The human visual system responds to light logarithmically, not linearly. Doubling the physical luminance of a surface does not look like doubling its brightness. It looks like a modest step at the bright end and a much larger one at the dark end. The eye is far more sensitive to changes in shadow than in highlights.

This is why a candle in a dark room stands out, and the same candle in daylight is simply invisible. It's all about relative light perception. The eye continuously adapts its sensitivity based on the surrounding luminance range, a process called adaptation, which means absolute luminance values matter far less than relative ones.

It also explains why HDR capture and tone mapping exist. No display can reproduce the full luminance range a scene contains, so the pipeline has to compress it in a way that approximates how the eye would have adapted to that scene in reality.

Linear Light vs Perceptual Brightness

Working in linear light means the renderer operates on physically correct values. Energy accumulates correctly - two lights of equal intensity produce twice the luminance at a surface. Falloff follows the inverse square law. Materials reflect the proportionally correct amount of incoming light.

Working in perceptual space feels more intuitive because it matches what the eye sees, but it breaks the physics. Add two perceptual brightness values together and the result is wrong. Apply a falloff curve and the math no longer describes reality.

This is the foundation behind the linear workflow and the Gamma Correction discussed in Day 16. The renderer works in linear to keep the physics intact; the gamma encode at the end maps the result back to something the display and the eye can interpret correctly.


Luminance Ranges in Real Scenes

To understand why tone mapping is necessary, it helps to have a sense of the actual luminance range involved:

The full range the adapted eye can handle spans roughly 10 orders of magnitude. No display or file format captures this. A typical SDR monitor tops out around 100-200 cd/m²; a high-end HDR display might reach 1,000–2,000 cd/m². The sun disk is still a billion times brighter.

Tone mapping is therefore a compression problem, not a correction problem. The goal is not to reproduce the scene accurately - that is physically impossible - but to compress the luminance range into something a display can show while preserving the perceptual relationships the eye would have experienced in the original scene.

Conclusion

Knowing real-world luminance ranges gives you a reference point for setting exposure and tone mapping parameters. Instead of eyeballing until it looks right, you can ask: does my sky sit in a plausible range relative to my shadowed surfaces? Does my interior feel correct relative to the window behind it?

The logarithmic perception point is also practically useful: small numerical changes in dark areas will read as larger perceptual shifts than the same change in bright areas. If your shadow detail is disappearing or your darks feel crushed, the issue is often less about the values themselves and more about where they land relative to the eye's adapted range.

With all the things I am learning regarding luminance, I would love to delve further into tone mapping and measuring luminance myself with a lightmeter and experiment with my own library. I still have a lot to learn about Exposure and how it translates into the digital domain.

Š 2026 Stefan Groenewoud - All views are my own, not those of my employer.

Day 6 - 🛠️ Behind the process - Approaching Material Creation

General / 07 May 2026

How I Approach Material Creation

How to approach material creation depends on whether it’s for personal work or a professional setting.

Personal vs. Professional

The goal shifts completely depending on context. For personal work, you’re trying to demonstrate your range and knowledge — you get to choose the subject matter, push yourself into unfamiliar territory, and show what you’re capable of. For professional work, the asset needs to serve the project first. If I’ve never worked on organic materials and want to explore them, a sci-fi corridor game is not the place to try it for the first time.

How Briefs Come In

I’ve worked from all kinds of starting points. Sometimes it’s a clear-cut piece of concept art with well-defined shapes, values, and a material breakdown already implied. Other times it’s a real-world reference that needs to be pushed toward the project’s visual language. The hardest and slowest is when the brief is open to interpretation. Multiple iterations, trying to translate someone else’s vision onto the canvas, until you land somewhere that feels right. If you can reduce ambiguity early, do it.

Starting Point: Alignment and Reference

On a professional project, my first step is always to align with the art director on their vision for the asset, prop, or environment piece. I don’t start building anything until that conversation has happened.

From there: gather a large pool of reference images. More than you think you need. Bring them to art direction and start culling: decide what stays, what goes, and what elements you’re specifically trying to hit. Reference isn’t decoration; it’s your contract with the team about what the final thing should feel like.

Art Direction

There is some unknown territory depending on whether you are the first artist on a project or joining one that already has a defined look. If you join an arena shooter that has existed for five years, you know that is the style you have to match - your job is to fit into an established system, not reinterpret it in your own way. Look-dev is mostly the time on a new project when a material artist and the art director will try to define the art style together and produce benchmark assets that set the example for the rest of production. In practice that means matching existing roughness ranges, staying within the established palette, and using the same base tiling rates as surrounding assets. The creative challenge shifts from "what should this look like" to "how do I get there within these rules."

For a personal project you are the art director, so you will have to figure out what look you're going for. That can be building toward a specific game company you'd love to work for, or simply setting yourself a new challenge.

I've never had to fully redo a material, but there have been moments where you have to reassess whether the direction you're heading is right for the intended look. A material can look very good in isolation, but in context it becomes too distracting. It might be a great portfolio piece on its own but it's also about striking a balance that best serves the project. That's not always easy.

Splitting It Up

Before diving in, it’s worth asking: is this one material, or a combination of materials and shading effects working together to produce the result? A wall might have a base plaster material, a damage decal layer, and a wetness mask, each is a separate concern. Getting that decomposition right early saves significant rework.

The choice usually comes down to this: a layered material approach gives you flexibility at runtime; think a slick oil layer on top of an asset that needs to shift over time, or a burn effect that sits on top of everything and is controlled by a placement mask. A single material is your typical export directly from Designer or Painter, but you need to account upfront for things like shader-driven tint, detail map blending, or any other parameters that need to be controlled at the engine level. Neither is always the right answer, it depends on how the asset will be used, how often it needs to vary, and what the shader budget allows.

The Actual Process

Set your texel density first. Figure out the tiling rate and set the real-world measurements in Substance Designer or Painter before you start building. This ensures that anything derived from your height map (normals, ambient occlusion, curvature) is physically grounded and PBR-compliant, and that your material sits consistently alongside everything else in the project at the same real-world scale.

Compartmentalize reusable features. If your project has rocks appearing across multiple surfaces — cliff faces, rubble piles, ground scatter; build a rock generator node once and share it across all materials that need it. The same goes for any recurring detail: pine needles, cracks, edge wear. You get visual consistency across the project and a single point to update when something needs to change.

Settle on a naming convention early. Your materials, resources, and shared nodes need to be findable: by you six months from now, and by whoever else is working in the same library. Whatever convention you pick, document it and stick to it.


Closing Thought

What works for one person doesn’t always translate to another’s workflow, so don’t be afraid to experiment and adapt. That said, on a professional project, you often don’t have the bandwidth to rethink your process mid-stream. Build the habits in personal work, so they’re second nature when it counts.

© 2026 Stefan Groenewoud — All views are my own, not those of my employer.

Day 4 - 💬 Take - Navigating the Creative Industry

General / 05 May 2026

Navigating the Creative Industry – My Experiences

The games industry is small. People talk. And yet, somehow, the recruitment process remains one of the least transparent parts of a career in this field.

This isn’t a guide on how to write a CV or ace an interview. It’s a collection of real experiences — things that actually happened, shared because I wish someone had told me this earlier. Whether you’re a newcomer or a seasoned professional looking for a change, I hope this saves you some time, frustration, and unnecessary self-doubt.

Studio Experiences

Studio A: Silence After Interest

A recruiter at a major triple-A studio reached out, the conversations went well, and then: nothing. I sent two or three follow-up emails. No response. Not even a “we’ve gone a different direction.”

It wasn’t just frustrating. It was unprofessional. In an industry this small, a recruiter is the face of the studio. Silence communicates more than they probably intend.

Takeaway: If a studio goes quiet after expressing interest, follow up once clearly, then move on. Their silence is information. And if you end up there later, you’ll remember how they treated candidates.

Studio B: The NDA Pressure

After multiple calls with another triple-A studio, they kept pushing for work samples I couldn’t share. The project hadn’t been announced. The work was under NDA; I told them this repeatedly. Their response: ”We could have a quick call so you can just briefly show it.”

Think about that. A company looking to hire trustworthy people, actively trying to get someone to breach a legal agreement. I’m certain they wouldn’t appreciate it if their own staff did the same.

Takeaway: A studio that pressures you to violate an NDA is showing you exactly how they operate. Walk away cleanly and without guilt.

Studio C: The Visa Oversight

Two interviews in. Good conversations. Then I had to chase the recruiter for a status update, only to find out they’d run into complications hiring me because of the visa process. Something they could have identified in the very first call.

Weeks of my time, and theirs, wasted on something that should have been a 5-minute check at the start.

Takeaway: Early in any process, ask directly: ”Are there any relocation, work authorisation, or visa constraints that could affect this role?” It protects everyone’s time. If a studio doesn’t have that answer upfront, that tells you something too.

Studio D: The Low-Ball Offer

A studio in a major European city extended an offer that barely covered basic living costs, let alone healthcare, in one of the most expensive cities in the world. I was more junior at the time, but the number was still disconnected from the reality of actually living there.

Takeaway: Always research cost of living against the offered salary before getting emotionally invested. Tools like Numbeo exist. Use them. Knowing your number going in puts you in a much stronger position and prevents the gut-punch of an offer that insults the city you’d be moving to.

Recruiter Experiences

Recruiter A: The Mentor Mirage

About ten or twelve years ago, I came across someone offering paid “mentor sessions” for newcomers to the industry. The pitch: guaranteed connections, 10+ years of recruitment experience, interview prep, resume polish, introductions to major studios.

When it came time to deliver on those introductions, they vanished.

It cost people money and, more importantly, time and trust during a vulnerable moment in their careers. To this day, I haven't seen this person deliver on the introductions they promised, and I haven't seen them working at any major studio since. In hindsight, there was probably more to the story than I knew at the time.

Takeaway: Vet anyone offering mentorship or career connections with the same rigour you’d apply to a job offer. Ask for specific examples. Ask who they’ve placed and where. Legitimate mentors welcome those questions.

Recruiter B: “It’s a Done Deal”

For a role at one of the largest studios in the US, the recruiter was enthusiastic to the point of being unprofessional. Phrases like ”it’s a done deal” and ”they’re lucky to have you in the process.” I went through the interviews. The team was great. Then I found out the job description didn’t actually match the role that was being filled.

Not only unprofessional; it’s a waste of the hiring team’s time and the candidate’s.

Takeaway: Treat recruiter enthusiasm as a yellow flag, not a green one. Ask direct, specific questions about the role’s responsibilities, team size, and reporting structure before investing in multiple interview rounds.

What I’ve Learned Overall

The recruitment process in games asks a lot of you: your time, your portfolio, your emotional energy. Not every studio or recruiter treats that investment with the respect it deserves.

A few things that have helped me:

  • Communicate your constraints early. Visa status, location, availability, salary expectations: raise these in the first conversation. It’s not impolite, it’s efficient.
  • Set your boundaries. It's okay to say no, or to step away from a process if something feels off.
  • Your NDA is not negotiable. Any studio worth working for will understand this immediately.
  • A recruiter’s enthusiasm is not an offer. Verify everything in writing.
  • Research the studio independently. Glassdoor, LinkedIn, people in your network who’ve worked there. The small industry works both ways; information travels.
  • Silence after engagement is an answer. Don’t chase indefinitely. One clear follow-up, then redirect your energy.

The industry is small. Your reputation matters, and so does theirs. Don’t be afraid to hold studios and recruiters to the same standard they hold you.

© 2026 Stefan Groenewoud — All views are my own, not those of my employer.

Refining Performant Levels of Detail (LOD)

General / 25 May 2025

Welcome back to the second part of our discussion on Level of Detail meshes! This blog post will be slightly shorter as it primarily builds upon the concepts introduced in the first post. In the first draft, we focused on calculating the LOD based on the diameter of an object. However, since the player in a game would see the mesh from various angles, it’s crucial to consider all viewpoints, not just one. 

Additionally, as someone pointed out in the comment section, this process assumes absolute values. To ensure your values are positive, simply wrap an Abs() function around them.

For instance, if we encounter assets from different angles, such as full frontal, full side view, or full top view, depending on their positioning and rotation by the artist, we shouldn’t only use one angle (the diagonal) for calculation. By taking into account all angles, we can give thin or flat objects a more realistic chance of being accurately measured. We collect all these values and calculate the mean as the input value. 

In hindsight, it might be better to use the maximum of all angles. If the difference between the smallest and largest number is too large, the LODs will fade out too quickly.

In an ideal setup, if the assets are placed and scaled, whether it’s larger or smaller, or if the field of view is wide or small, the game engine should compensate for these factors accordingly.


pseudo code


diagonal = ∛(0.075² + 0.157² + 0.167²) = 0.387m
front = ∛(0.075² + 0.157²)  = 0.312m
side = ∛(0.157² + 0.167²)  = 0.375m
top = ∛(0.075² + 0.167²)  = 0.322m


mean = ((diagonal + front + side + top) / 4)  = 0.349m


CalculateDistance(inSize=mean, inScreenSpacePercentage=100, inFovDegrees=45.0, inVerticalResolution=1080):
   ratio = inVerticalResolution / 1080 # compensate for 4k or FullHD screens
   # distance to see the asset, top to bottom
   distance_to_object = (inSize * 0.5) / (tan(inFovDegrees * 0.5))
   distance_by_screensize = distance_to_object * 100 / inScreenSpacePercentage
   max_distance = distance_by_screensize * ratio
   return max_distance


Horizon Forbidden West: Automated Asset Conversion And Updating

General / 06 March 2025

This postmortem analysis delves into my approach to swiftly converting a substantial amount of assets. Picture this scenario: when transitioning from Horizon Zero Dawn, the initial game, to its sequel, Horizon Forbidden West, we encountered the task of updating the game’s asset-art (props and environment-art models) to reflect the latest technological advancements and ensure compliance with the technical specifications. Moreover, we had to replace the assigned shaders with novel ones specifically designed for Horizon Forbidden West. The sequel would leverage existing assets while introducing new ones. Consequently, some of these scripts and pipelines were reusable. Given the sheer number of assets we wanted to display on screen, we prioritized optimizing their setup to stay within the memory budgets while maintaining the visual fidelity of the first game, supporting an approximately 7-year-old platform (PS4) and the (at the time) newly released PS5.

Step-by-step

  • Asset Management: Collected, tagged, and tracked over 5000 assets in a local database, highlighting the importance of testing and validating changes.
  • Texture Optimization: Optimized textures by discarding unnecessary maps, converting PSDs to PNGs, and ensuring consistency in materials like rock and stone.
  • Conversion Process: Utilized the Substance Automation Toolkit API and a shared Python library for texture conversion, ensuring PBR compliance and compatibility with the engine.
  • Maya: Executed Maya batch script, providing information and updating shaders, and relinking textures.
  • Export: Exported assets to the engine and linked to a test level for evaluation purposes.
  • Evaluating: Evaluated GPU performance, export/conversion issues, and internal tools/settings in-game.

I initiated the process by collecting all assets that required updating through a script. This step allowed me to assess the scope of the undertaking. I tagged and tracked each asset in my local database (a JSON file) with the appropriate process, the Maya file associated with that asset, all the textures linked to the asset, and some other data that I can’t quite remember right now.

Side note:

However, it’s important to mention that this approach wasn’t entirely foolproof and a one-size-fits-all solution. With over 5,000 files touched, all of which were linked to levels, sets, cinematics, or prefabs, the process became even more fragile and complicated, particularly during production when everyone was striving to complete the game. Coordinating the initiative poses a challenge, emphasizing the significance of allocating sufficient time for testing and validating the changes. To address this, it’s crucial to split the changes into smaller check-ins.

Let’s continue!

While updating the files, I took the liberty of optimizing content whenever possible. I discarded any Specular maps that weren’t necessary for dielectrics or any other unnecessary maps. Determining the need for dedicated Specular channels could be challenging, especially since not everything was authored using PBR techniques. Relying solely on Python libraries and image processing wasn’t always sufficient. Additionally, I converted all PSDs to PNGs to significantly enhance Perforce syncing times, image processing, and DDS exporting times.

In addition to texture optimization, we also had to identify assets that required recoloring treatment. For instance, if an asset contained rock or stone materials, we wanted to ensure that it (visually and color-wise) matched the other rocks by applying the same coloring treatment. In most cases, I was able to quickly generate a mask for other assets through a manual process. I could either extract it from the PSD layers, bake out the UV layout and use that, or simply mask in Photoshop and have that linked during the Substance file generation process.

Conversion

To begin the texture conversion process, I utilized the Substance Automation Toolkit API. Certain aspects of this process were defined in a shared Python library that I had written for it. This library is explained in more detail in another blog post titled “Texturing for Rocks.” This shared graph would clamp the Color values to be more PBR compliant, clamp the Roughness ranges due to the engine shading model, and run the AO through a Curve node to compensate for some manually authored AO that was too dark.

Why the Substance Automation Toolkit? Creating a new file using the API is a straightforward process. It’s easy to replicate, and we can open the Substance file in Designer and export it if necessary. Batch exporting is also an option if any changes are made to the shared Substance graph.

Maya

The next step was to update the textures linked in the engine and reexport the DDS’s. This was accomplished using the internally created library, which resulted in a relatively fast process.

Initially, I attempted to use a headless Maya version, but this approach did not work because the viewport renderer did not initialize the shaders and shader-defined information required for evaluation and update. As a slower alternative, I opted to run regular Maya in a batch process. In this process, I provided Maya with a list of information per Maya scene and updated the shaders. Upon loading, I retained the old variables in memory, updated the shader, and then updated the variables accordingly, compensating for any differences, such as variable names or ratios, like tiling for detail maps. Finally, I relinked the textures to the shader once more, ensuring they were correctly associated with the mesh.

From there, the assets were exported to the engine. Unfortunately, I cannot provide further details due to the proprietary nature of the engine.

Once the assets were exported, I was able to link them to my test level, enabling me to evaluate the performance (GPU) in-game and assess any issues that may have arisen during export or the conversion process. Before checking in, I conducted a quick test process to ensure everything was working as expected. I used our internal tools and double-checked if none of the previously set settings or draw distances were broken. The further into development you are, the more vital this becomes.

Conclusion

The way I present the steps in my explanation is also how the process functioned in Python. Evaluating images using PIL was the quickest step, while Maya posed the most significant challenge. Re-exporting assets from Maya was the slowest and most prone to crashes. Multiprocessing consumed so much memory that I experienced several blue screens. It was a valuable learning experience, as I learned how to automate certain steps and refine the Python code to be more compatible in different scenarios rather than a one-time use solution. However, it became apparent that full automation for this process was too risky due to the many odd cases and asset setups that had to be validated by eye. 

The ideations and iterations that emerged from these processes, served and continue to serve as the foundation for more automated and/or procedurally driven processes.

A special thanks to Chris Thompson for proofreading and to Guerrilla for allowing me to publish and share this information.

Writing technical documentation for production readiness

General / 03 February 2025

In this blog post, I’ll share my thoughts and expertise on the under appreciated yet crucial process of writing documentation within the industry. I’m referring to the broader implications of outsourcing assets and texturing pipelines, excluding the financial aspects since they’re not my area of expertise. This topic likely extends far beyond the scope of a single blog post, and I may delve deeper into it over time.

Objective of Documentation

Clear documentation is essential for internal teams, new members (including partners and outsourcing vendors), and project management. It serves as a comprehensive reference for project information and workflow, eliminating misunderstandings and inefficiencies that save time and money. No one wants a tool or system set up by someone who left the company without proper documentation, leaving them uncertain about its limitations, setup, and potential improvements.

The key to its success lies in regular updates. This ensures that outdated or obsolete information doesn’t accumulate, potentially leading to code rot. Documentation should be detailed enough to explain the main process but concise enough to be easily accessible when needed, such as onboarding a new developer or updating it.

Analysis

  • Ambiguity regarding the documentation’s intended purpose
  • Inconsistent terminology causes confusion
  • Repetitive information encourages skimming rather than thorough reading
  • Information scattered across various locations 
  • Is it sufficiently clear to non-native English speakers (depending on your team and your vendors)?

Iteration

The following points will undergo a continuous process of iteration and evaluation in collaboration with your collaborators and team members. The ever-changing landscape of tools, workflows, and tech needs us to keep adapting and improving our development process.

  • Thoroughly review all the documentation, following each documented step-by-step to ensure its accuracy and identify any missing information.
  • Conduct buddy checks to verify that everything makes sense from multiple perspectives.
  • Organize the content based on its complexity, considering that not all artists need or would be comfortable exploring more complex topics.
  • The information is available, but it is currently separated from the fundamental concepts.
  • Evaluate any workflow issues, blockers, or missing features in shared content, such as shaders or textures/material library.
  • Understand your target audience: Are you writing the documentation for a developer who is expected to have basic knowledge of a tool, software, or engine, or are you writing it for someone who has never used it before?

Lessons Learned

If I were to start fresh, I would improve the approach by adding more clear images, callouts, or even videos to reduce confusion, which is especially helpful for visual learners. Before starting the project, I would suggest creating a layout and showing a proof-of-concept for the potential setup. Furthermore, collaborating with different departments will help us delegate and coordinate specific documentation tasks.

Example

Texturing & Materials (Main Category)

---> Basic Workflow (Subcategory)

    -> Simple Material Blend Workflow

        -> Bespoke Texturing Workflow

    -> Advanced Workflow

        -> Height Blend Workflow

Conclusion

Be open to rewriting, re-editing, and redoing a substantial portion of the initially written documentation. Don’t be overly attached to what’s been written. With a well-designed revisioning system, you can always undo any changes or unintended problems. View this as an opportunity to reflect on your personal growth and improvements throughout the process.